|
@@ -22,6 +22,11 @@ private enum MeetingProvider: Int {
|
|
|
case zoho = 3
|
|
case zoho = 3
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+private enum ZoomJoinMode: Int {
|
|
|
|
|
+ case id = 0
|
|
|
|
|
+ case url = 1
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
private enum SettingsAction: Int {
|
|
private enum SettingsAction: Int {
|
|
|
case restore = 0
|
|
case restore = 0
|
|
|
case rateUs = 1
|
|
case rateUs = 1
|
|
@@ -48,9 +53,12 @@ final class ViewController: NSViewController {
|
|
|
private var tabViews: [MeetingProvider: NSView] = [:]
|
|
private var tabViews: [MeetingProvider: NSView] = [:]
|
|
|
private var selectedSidebarPage: SidebarPage = .joinMeetings
|
|
private var selectedSidebarPage: SidebarPage = .joinMeetings
|
|
|
private var selectedMeetingProvider: MeetingProvider = .meet
|
|
private var selectedMeetingProvider: MeetingProvider = .meet
|
|
|
|
|
+ private var selectedZoomJoinMode: ZoomJoinMode = .id
|
|
|
private var pageCache: [SidebarPage: NSView] = [:]
|
|
private var pageCache: [SidebarPage: NSView] = [:]
|
|
|
private var sidebarPageByView = [ObjectIdentifier: SidebarPage]()
|
|
private var sidebarPageByView = [ObjectIdentifier: SidebarPage]()
|
|
|
private var meetingProviderByView = [ObjectIdentifier: MeetingProvider]()
|
|
private var meetingProviderByView = [ObjectIdentifier: MeetingProvider]()
|
|
|
|
|
+ private var zoomJoinModeByView = [ObjectIdentifier: ZoomJoinMode]()
|
|
|
|
|
+ private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
|
|
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
|
private weak var centeredTitleLabel: NSTextField?
|
|
private weak var centeredTitleLabel: NSTextField?
|
|
|
private weak var paywallWindow: NSWindow?
|
|
private weak var paywallWindow: NSWindow?
|
|
@@ -166,7 +174,26 @@ private extension ViewController {
|
|
|
let provider = meetingProviderByView[ObjectIdentifier(view)],
|
|
let provider = meetingProviderByView[ObjectIdentifier(view)],
|
|
|
provider != selectedMeetingProvider else { return }
|
|
provider != selectedMeetingProvider else { return }
|
|
|
selectedMeetingProvider = provider
|
|
selectedMeetingProvider = provider
|
|
|
|
|
+ if provider == .zoom {
|
|
|
|
|
+ selectedZoomJoinMode = .id
|
|
|
|
|
+ }
|
|
|
updateTabAppearance()
|
|
updateTabAppearance()
|
|
|
|
|
+ if selectedSidebarPage == .joinMeetings {
|
|
|
|
|
+ pageCache[.joinMeetings] = nil
|
|
|
|
|
+ showSidebarPage(.joinMeetings)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func zoomJoinModeClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
|
|
+ guard let view = sender.view,
|
|
|
|
|
+ let mode = zoomJoinModeByView[ObjectIdentifier(view)],
|
|
|
|
|
+ mode != selectedZoomJoinMode else { return }
|
|
|
|
|
+ selectedZoomJoinMode = mode
|
|
|
|
|
+ updateZoomJoinModeAppearance()
|
|
|
|
|
+ if selectedSidebarPage == .joinMeetings, selectedMeetingProvider == .zoom {
|
|
|
|
|
+ pageCache[.joinMeetings] = nil
|
|
|
|
|
+ showSidebarPage(.joinMeetings)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
|
|
@objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
|
|
@@ -555,8 +582,17 @@ private extension ViewController {
|
|
|
|
|
|
|
|
contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary))
|
|
contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary))
|
|
|
contentStack.addArrangedSubview(meetingTypeTabs())
|
|
contentStack.addArrangedSubview(meetingTypeTabs())
|
|
|
- contentStack.addArrangedSubview(joinWithURLHeading())
|
|
|
|
|
- contentStack.addArrangedSubview(meetingUrlSection())
|
|
|
|
|
|
|
+ if selectedMeetingProvider == .zoom {
|
|
|
|
|
+ contentStack.addArrangedSubview(zoomJoinModeTabs())
|
|
|
|
|
+ if selectedZoomJoinMode == .id {
|
|
|
|
|
+ contentStack.addArrangedSubview(zoomMeetingIDSection())
|
|
|
|
|
+ } else {
|
|
|
|
|
+ contentStack.addArrangedSubview(meetingUrlSection())
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ contentStack.addArrangedSubview(joinWithURLHeading())
|
|
|
|
|
+ contentStack.addArrangedSubview(meetingUrlSection())
|
|
|
|
|
+ }
|
|
|
contentStack.addArrangedSubview(scheduleHeader())
|
|
contentStack.addArrangedSubview(scheduleHeader())
|
|
|
contentStack.addArrangedSubview(textLabel("Tuesday, 14 Apr", font: typography.dateHeading, color: palette.textSecondary))
|
|
contentStack.addArrangedSubview(textLabel("Tuesday, 14 Apr", font: typography.dateHeading, color: palette.textSecondary))
|
|
|
contentStack.addArrangedSubview(scheduleCardsRow())
|
|
contentStack.addArrangedSubview(scheduleCardsRow())
|
|
@@ -911,6 +947,196 @@ private extension ViewController {
|
|
|
return card
|
|
return card
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ func zoomJoinModeTabs() -> NSView {
|
|
|
|
|
+ let row = NSStackView()
|
|
|
|
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ row.orientation = .horizontal
|
|
|
|
|
+ row.alignment = .centerY
|
|
|
|
|
+ row.spacing = 28
|
|
|
|
|
+ row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let idTab = joinModeTab("Join with ID", mode: .id)
|
|
|
|
|
+ let urlTab = joinModeTab("Join with URL", mode: .url)
|
|
|
|
|
+ row.addArrangedSubview(idTab)
|
|
|
|
|
+ row.addArrangedSubview(urlTab)
|
|
|
|
|
+ let spacer = NSView()
|
|
|
|
|
+ spacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ row.addArrangedSubview(spacer)
|
|
|
|
|
+
|
|
|
|
|
+ zoomJoinModeViews[.id] = idTab
|
|
|
|
|
+ zoomJoinModeViews[.url] = urlTab
|
|
|
|
|
+ updateZoomJoinModeAppearance()
|
|
|
|
|
+ return row
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func joinModeTab(_ title: String, mode: ZoomJoinMode) -> NSView {
|
|
|
|
|
+ let tab = HoverTrackingView()
|
|
|
|
|
+ tab.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ tab.wantsLayer = true
|
|
|
|
|
+ tab.layer?.cornerRadius = 6
|
|
|
|
|
+ tab.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
|
|
+ tab.heightAnchor.constraint(equalToConstant: 30).isActive = true
|
|
|
|
|
+ zoomJoinModeByView[ObjectIdentifier(tab)] = mode
|
|
|
|
|
+
|
|
|
|
|
+ let label = textLabel(title, font: NSFont.systemFont(ofSize: 33 / 2, weight: .medium), color: palette.textPrimary)
|
|
|
|
|
+ tab.addSubview(label)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ label.leadingAnchor.constraint(equalTo: tab.leadingAnchor, constant: 4),
|
|
|
|
|
+ label.trailingAnchor.constraint(equalTo: tab.trailingAnchor, constant: -4),
|
|
|
|
|
+ label.topAnchor.constraint(equalTo: tab.topAnchor, constant: 4),
|
|
|
|
|
+ label.bottomAnchor.constraint(equalTo: tab.bottomAnchor, constant: -6)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let click = NSClickGestureRecognizer(target: self, action: #selector(zoomJoinModeClicked(_:)))
|
|
|
|
|
+ tab.addGestureRecognizer(click)
|
|
|
|
|
+ return tab
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func updateZoomJoinModeAppearance() {
|
|
|
|
|
+ for (mode, tab) in zoomJoinModeViews {
|
|
|
|
|
+ let selected = (mode == selectedZoomJoinMode)
|
|
|
|
|
+ let textColor = selected ? palette.textPrimary : palette.textSecondary
|
|
|
|
|
+ let label = tab.subviews.first { $0 is NSTextField } as? NSTextField
|
|
|
|
|
+ label?.textColor = textColor
|
|
|
|
|
+
|
|
|
|
|
+ // Keep the active tab visually underlined like the reference.
|
|
|
|
|
+ if selected {
|
|
|
|
|
+ if tab.subviews.contains(where: { $0.identifier?.rawValue == "modeUnderline" }) == false {
|
|
|
|
|
+ let underline = NSView()
|
|
|
|
|
+ underline.identifier = NSUserInterfaceItemIdentifier("modeUnderline")
|
|
|
|
|
+ underline.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ underline.wantsLayer = true
|
|
|
|
|
+ underline.layer?.backgroundColor = palette.primaryBlue.cgColor
|
|
|
|
|
+ tab.addSubview(underline)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ underline.leadingAnchor.constraint(equalTo: tab.leadingAnchor),
|
|
|
|
|
+ underline.trailingAnchor.constraint(equalTo: tab.trailingAnchor),
|
|
|
|
|
+ underline.bottomAnchor.constraint(equalTo: tab.bottomAnchor),
|
|
|
|
|
+ underline.heightAnchor.constraint(equalToConstant: 2)
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tab.subviews
|
|
|
|
|
+ .filter { $0.identifier?.rawValue == "modeUnderline" }
|
|
|
|
|
+ .forEach { $0.removeFromSuperview() }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func joinWithIDHeading() -> NSView {
|
|
|
|
|
+ let container = NSView()
|
|
|
|
|
+ container.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let title = textLabel("Join with ID", font: typography.joinWithURLTitle, color: palette.textPrimary)
|
|
|
|
|
+ title.alignment = .left
|
|
|
|
|
+ title.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
|
|
+ title.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
|
|
+
|
|
|
|
|
+ let bar = NSView()
|
|
|
|
|
+ bar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ bar.wantsLayer = true
|
|
|
|
|
+ bar.layer?.backgroundColor = palette.primaryBlue.cgColor
|
|
|
|
|
+ bar.heightAnchor.constraint(equalToConstant: 3).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ container.addSubview(title)
|
|
|
|
|
+ container.addSubview(bar)
|
|
|
|
|
+
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ title.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
|
|
|
+ title.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ bar.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
|
|
|
+ bar.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6),
|
|
|
|
|
+ bar.widthAnchor.constraint(equalTo: title.widthAnchor),
|
|
|
|
|
+ bar.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ container.trailingAnchor.constraint(equalTo: title.trailingAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ return container
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func zoomMeetingIDSection() -> NSView {
|
|
|
|
|
+ let wrapper = NSView()
|
|
|
|
|
+ wrapper.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let fieldsRow = NSStackView()
|
|
|
|
|
+ fieldsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ fieldsRow.orientation = .horizontal
|
|
|
|
|
+ fieldsRow.alignment = .top
|
|
|
|
|
+ fieldsRow.distribution = .fillEqually
|
|
|
|
|
+ fieldsRow.spacing = 12
|
|
|
|
|
+ fieldsRow.addArrangedSubview(zoomInputField(title: "Meeting ID", placeholder: "Enter meeting ID..."))
|
|
|
|
|
+ fieldsRow.addArrangedSubview(zoomInputField(title: "Meeting Passcode", placeholder: "Enter meeting passcode..."))
|
|
|
|
|
+
|
|
|
|
|
+ let actions = NSStackView()
|
|
|
|
|
+ actions.orientation = .horizontal
|
|
|
|
|
+ actions.spacing = 10
|
|
|
|
|
+ actions.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ actions.alignment = .centerY
|
|
|
|
|
+ actions.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110))
|
|
|
|
|
+ actions.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116))
|
|
|
|
|
+
|
|
|
|
|
+ wrapper.addSubview(fieldsRow)
|
|
|
|
|
+ wrapper.addSubview(actions)
|
|
|
|
|
+
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780),
|
|
|
|
|
+
|
|
|
|
|
+ fieldsRow.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
|
|
|
|
+ fieldsRow.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
|
|
|
|
|
+ fieldsRow.topAnchor.constraint(equalTo: wrapper.topAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ actions.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
|
|
|
|
|
+ actions.topAnchor.constraint(equalTo: fieldsRow.bottomAnchor, constant: 14),
|
|
|
|
|
+ actions.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ return wrapper
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func zoomInputField(title: String, placeholder: String) -> NSView {
|
|
|
|
|
+ let wrapper = NSView()
|
|
|
|
|
+ wrapper.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let heading = textLabel(title, font: typography.fieldLabel, color: palette.textPrimary)
|
|
|
|
|
+ let textFieldContainer = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
|
|
|
|
|
+ textFieldContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ textFieldContainer.heightAnchor.constraint(equalToConstant: 40).isActive = true
|
|
|
|
|
+ styleSurface(textFieldContainer, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
|
|
+
|
|
|
|
|
+ let field = NSTextField(string: "")
|
|
|
|
|
+ field.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ field.isEditable = true
|
|
|
|
|
+ field.isSelectable = true
|
|
|
|
|
+ field.isBordered = false
|
|
|
|
|
+ field.drawsBackground = false
|
|
|
|
|
+ field.placeholderString = placeholder
|
|
|
|
|
+ field.font = typography.inputPlaceholder
|
|
|
|
|
+ field.textColor = palette.textPrimary
|
|
|
|
|
+ field.focusRingType = .none
|
|
|
|
|
+ textFieldContainer.addSubview(field)
|
|
|
|
|
+
|
|
|
|
|
+ wrapper.addSubview(heading)
|
|
|
|
|
+ wrapper.addSubview(textFieldContainer)
|
|
|
|
|
+
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ heading.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
|
|
|
|
+ heading.topAnchor.constraint(equalTo: wrapper.topAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ textFieldContainer.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
|
|
|
|
+ textFieldContainer.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
|
|
|
|
|
+ textFieldContainer.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 10),
|
|
|
|
|
+ textFieldContainer.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ field.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 12),
|
|
|
|
|
+ field.trailingAnchor.constraint(equalTo: textFieldContainer.trailingAnchor, constant: -12),
|
|
|
|
|
+ field.centerYAnchor.constraint(equalTo: textFieldContainer.centerYAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ return wrapper
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
func joinWithURLHeading() -> NSView {
|
|
func joinWithURLHeading() -> NSView {
|
|
|
let container = NSView()
|
|
let container = NSView()
|
|
|
container.translatesAutoresizingMaskIntoConstraints = false
|
|
container.translatesAutoresizingMaskIntoConstraints = false
|