|
|
@@ -194,9 +194,10 @@ class ViewController: NSViewController {
|
|
|
private weak var scheduleTopicField: NSTextField?
|
|
|
private weak var scheduleDateField: NSTextField?
|
|
|
private weak var scheduleTimeField: NSTextField?
|
|
|
- private weak var scheduleTimeZoneCombo: NSComboBox?
|
|
|
+ private weak var scheduleTimeZonePopup: NSPopUpButton?
|
|
|
private weak var scheduleDurationField: NSTextField?
|
|
|
private weak var scheduleSubmitButton: NSButton?
|
|
|
+ private var scheduleTimeZoneDisplayToIdentifier: [String: String] = [:]
|
|
|
private weak var joinURLField: NSTextField?
|
|
|
private weak var joinMeetingIDField: NSTextField?
|
|
|
private weak var joinPasscodeField: NSTextField?
|
|
|
@@ -670,19 +671,26 @@ class ViewController: NSViewController {
|
|
|
let timeHint = makeLabel("12-hour time in the timezone below · example 2:30 PM", size: 11, color: mutedText, weight: .regular, centered: false)
|
|
|
|
|
|
let tzLabel = makeLabel("Timezone", size: 12, color: secondaryText, weight: .medium, centered: false)
|
|
|
- let tzCombo = NSComboBox()
|
|
|
+ let tzCombo = NSPopUpButton()
|
|
|
tzCombo.translatesAutoresizingMaskIntoConstraints = false
|
|
|
tzCombo.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
- tzCombo.textColor = primaryText
|
|
|
- tzCombo.backgroundColor = palette.inputBackground
|
|
|
- tzCombo.isSelectable = true
|
|
|
- tzCombo.completes = true
|
|
|
- tzCombo.numberOfVisibleItems = 10
|
|
|
- tzCombo.addItems(withObjectValues: TimeZone.knownTimeZoneIdentifiers.sorted())
|
|
|
- tzCombo.stringValue = TimeZone.current.identifier
|
|
|
- tzCombo.toolTip = "IANA timezone (type to filter). Meeting start is interpreted in this zone."
|
|
|
- scheduleTimeZoneCombo = tzCombo
|
|
|
- let tzHint = makeLabel("Type to search, e.g. America/New_York or Europe/London", size: 11, color: mutedText, weight: .regular, centered: false)
|
|
|
+ tzCombo.wantsLayer = true
|
|
|
+ tzCombo.layer?.cornerRadius = 10
|
|
|
+ tzCombo.layer?.borderWidth = 1
|
|
|
+ tzCombo.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
+ tzCombo.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
+ let tzOptions = makeTimeZoneDisplayOptions(referenceDate: defaultStart)
|
|
|
+ scheduleTimeZoneDisplayToIdentifier = Dictionary(uniqueKeysWithValues: tzOptions.map { ($0.display, $0.identifier) })
|
|
|
+ tzCombo.removeAllItems()
|
|
|
+ tzCombo.addItems(withTitles: tzOptions.map(\.display))
|
|
|
+ if let selected = tzOptions.first(where: { $0.identifier == TimeZone.current.identifier }) {
|
|
|
+ tzCombo.selectItem(withTitle: selected.display)
|
|
|
+ } else {
|
|
|
+ tzCombo.selectItem(at: 0)
|
|
|
+ }
|
|
|
+ tzCombo.toolTip = "Timezone with GMT offset. Meeting start is interpreted in this zone."
|
|
|
+ scheduleTimeZonePopup = tzCombo
|
|
|
+ let tzHint = makeLabel("Includes GMT offset, e.g. GMT+05:00 - Asia/Karachi", size: 11, color: mutedText, weight: .regular, centered: false)
|
|
|
|
|
|
let tzStack = NSStackView(views: [tzLabel, tzCombo, tzHint])
|
|
|
tzStack.orientation = .vertical
|
|
|
@@ -789,7 +797,7 @@ class ViewController: NSViewController {
|
|
|
|
|
|
topicBox.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
|
tzCombo.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
|
- tzCombo.heightAnchor.constraint(equalToConstant: 46),
|
|
|
+ tzCombo.heightAnchor.constraint(equalToConstant: 34),
|
|
|
dateTimeRow.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
|
durationBox.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
|
buttons.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
|
@@ -811,11 +819,13 @@ class ViewController: NSViewController {
|
|
|
showSimpleAlert(title: "Topic required", message: "Enter a name for your meeting.")
|
|
|
return
|
|
|
}
|
|
|
- let tzId = scheduleTimeZoneCombo?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
- if tzId.isEmpty == false, TimeZone(identifier: tzId) == nil {
|
|
|
+ let tzId = scheduleTimeZonePopup?.titleOfSelectedItem?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
+ if tzId.isEmpty == false,
|
|
|
+ scheduleTimeZoneDisplayToIdentifier[tzId] == nil,
|
|
|
+ TimeZone(identifier: tzId) == nil {
|
|
|
showSimpleAlert(
|
|
|
title: "Timezone",
|
|
|
- message: "Enter a valid IANA timezone (pick from the list or type to search), for example America/New_York."
|
|
|
+ message: "Choose a timezone from the list (with GMT offset), for example GMT+05:00 - Asia/Karachi."
|
|
|
)
|
|
|
return
|
|
|
}
|
|
|
@@ -896,18 +906,45 @@ class ViewController: NSViewController {
|
|
|
scheduleTopicField = nil
|
|
|
scheduleDateField = nil
|
|
|
scheduleTimeField = nil
|
|
|
- scheduleTimeZoneCombo = nil
|
|
|
+ scheduleTimeZonePopup = nil
|
|
|
scheduleDurationField = nil
|
|
|
scheduleSubmitButton = nil
|
|
|
+ scheduleTimeZoneDisplayToIdentifier = [:]
|
|
|
}
|
|
|
|
|
|
/// Resolves the schedule panel timezone (IANA id from combo, or system default).
|
|
|
private func resolvedScheduleTimeZoneForMeeting() -> TimeZone {
|
|
|
- let raw = scheduleTimeZoneCombo?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
+ let raw = scheduleTimeZonePopup?.titleOfSelectedItem?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
+ if let identifier = scheduleTimeZoneDisplayToIdentifier[raw],
|
|
|
+ let mapped = TimeZone(identifier: identifier) {
|
|
|
+ return mapped
|
|
|
+ }
|
|
|
guard raw.isEmpty == false, let tz = TimeZone(identifier: raw) else { return TimeZone.current }
|
|
|
return tz
|
|
|
}
|
|
|
|
|
|
+ private struct TimeZoneDisplayOption {
|
|
|
+ let identifier: String
|
|
|
+ let display: String
|
|
|
+ }
|
|
|
+
|
|
|
+ private func makeTimeZoneDisplayOptions(referenceDate: Date) -> [TimeZoneDisplayOption] {
|
|
|
+ TimeZone.knownTimeZoneIdentifiers.compactMap { identifier in
|
|
|
+ guard let tz = TimeZone(identifier: identifier) else { return nil }
|
|
|
+ let seconds = tz.secondsFromGMT(for: referenceDate)
|
|
|
+ let sign = seconds >= 0 ? "+" : "-"
|
|
|
+ let absSeconds = abs(seconds)
|
|
|
+ let hours = absSeconds / 3600
|
|
|
+ let minutes = (absSeconds % 3600) / 60
|
|
|
+ let offset = String(format: "GMT%@%02d:%02d", sign, hours, minutes)
|
|
|
+ return TimeZoneDisplayOption(identifier: identifier, display: "\(offset) - \(identifier)")
|
|
|
+ }
|
|
|
+ .sorted { lhs, rhs in
|
|
|
+ if lhs.display == rhs.display { return lhs.identifier < rhs.identifier }
|
|
|
+ return lhs.display < rhs.display
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private struct ScheduleMeetingDraft {
|
|
|
let startDate: Date
|
|
|
let startTimeWithOffset: String
|