|
@@ -266,6 +266,7 @@ final class ViewController: NSViewController {
|
|
|
private var zoomJoinModeByView = [ObjectIdentifier: ZoomJoinMode]()
|
|
private var zoomJoinModeByView = [ObjectIdentifier: ZoomJoinMode]()
|
|
|
private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
|
|
private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
|
|
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
|
|
|
+ private var aiCompanionAudioURLByView = [ObjectIdentifier: URL]()
|
|
|
private var paywallWindow: NSWindow?
|
|
private var paywallWindow: NSWindow?
|
|
|
private weak var paywallOverlayView: NSView?
|
|
private weak var paywallOverlayView: NSView?
|
|
|
private let paywallContentWidth: CGFloat = 520
|
|
private let paywallContentWidth: CGFloat = 520
|
|
@@ -1944,6 +1945,24 @@ private extension ViewController {
|
|
|
panel.translatesAutoresizingMaskIntoConstraints = false
|
|
panel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
panel.userInterfaceLayoutDirection = .leftToRight
|
|
panel.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
|
|
|
|
|
+ let scroll = NSScrollView()
|
|
|
|
|
+ scroll.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ scroll.drawsBackground = false
|
|
|
|
|
+ scroll.hasHorizontalScroller = false
|
|
|
|
|
+ scroll.hasVerticalScroller = true
|
|
|
|
|
+ scroll.autohidesScrollers = true
|
|
|
|
|
+ scroll.borderType = .noBorder
|
|
|
|
|
+ scroll.scrollerStyle = .overlay
|
|
|
|
|
+ scroll.automaticallyAdjustsContentInsets = false
|
|
|
|
|
+ let clip = TopAlignedClipView()
|
|
|
|
|
+ clip.drawsBackground = false
|
|
|
|
|
+ scroll.contentView = clip
|
|
|
|
|
+ panel.addSubview(scroll)
|
|
|
|
|
+
|
|
|
|
|
+ let content = NSView()
|
|
|
|
|
+ content.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ scroll.documentView = content
|
|
|
|
|
+
|
|
|
let contentStack = NSStackView()
|
|
let contentStack = NSStackView()
|
|
|
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
contentStack.userInterfaceLayoutDirection = .leftToRight
|
|
contentStack.userInterfaceLayoutDirection = .leftToRight
|
|
@@ -1987,12 +2006,23 @@ private extension ViewController {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- panel.addSubview(contentStack)
|
|
|
|
|
|
|
+ content.addSubview(contentStack)
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
- contentStack.leftAnchor.constraint(equalTo: panel.leftAnchor, constant: 28),
|
|
|
|
|
- contentStack.rightAnchor.constraint(equalTo: panel.rightAnchor, constant: -28),
|
|
|
|
|
- contentStack.topAnchor.constraint(equalTo: panel.topAnchor),
|
|
|
|
|
- contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -16)
|
|
|
|
|
|
|
+ scroll.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
|
|
|
|
|
+ scroll.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
|
|
|
|
|
+ scroll.topAnchor.constraint(equalTo: panel.topAnchor),
|
|
|
|
|
+ scroll.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ content.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
|
|
|
|
|
+ content.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
|
|
|
|
|
+ content.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
|
|
|
|
|
+ content.bottomAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.bottomAnchor),
|
|
|
|
|
+ content.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ contentStack.leftAnchor.constraint(equalTo: content.leftAnchor, constant: 28),
|
|
|
|
|
+ contentStack.rightAnchor.constraint(equalTo: content.rightAnchor, constant: -28),
|
|
|
|
|
+ contentStack.topAnchor.constraint(equalTo: content.topAnchor),
|
|
|
|
|
+ contentStack.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -16)
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
return panel
|
|
return panel
|
|
@@ -2019,14 +2049,27 @@ private extension ViewController {
|
|
|
dateLabel.alignment = .left
|
|
dateLabel.alignment = .left
|
|
|
|
|
|
|
|
let audioLink = mockAudioURLString(for: meeting)
|
|
let audioLink = mockAudioURLString(for: meeting)
|
|
|
- let audioLabel = textLabel("Mock Audio: \(audioLink)", font: typography.fieldLabel, color: palette.primaryBlue)
|
|
|
|
|
- audioLabel.alignment = .left
|
|
|
|
|
- audioLabel.maximumNumberOfLines = 2
|
|
|
|
|
- audioLabel.lineBreakMode = .byTruncatingTail
|
|
|
|
|
|
|
+ let audioButton = NSButton(title: "Play Audio", target: self, action: #selector(aiCompanionAudioTapped(_:)))
|
|
|
|
|
+ audioButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ audioButton.isBordered = false
|
|
|
|
|
+ audioButton.bezelStyle = .inline
|
|
|
|
|
+ audioButton.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
|
|
|
|
|
+ audioButton.contentTintColor = palette.primaryBlue
|
|
|
|
|
+ audioButton.alignment = .left
|
|
|
|
|
+ audioButton.setButtonType(.momentaryPushIn)
|
|
|
|
|
+ if let audioURL = URL(string: audioLink) {
|
|
|
|
|
+ aiCompanionAudioURLByView[ObjectIdentifier(audioButton)] = audioURL
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let audioURLLabel = textLabel(audioLink, font: typography.fieldLabel, color: palette.textMuted)
|
|
|
|
|
+ audioURLLabel.alignment = .left
|
|
|
|
|
+ audioURLLabel.maximumNumberOfLines = 2
|
|
|
|
|
+ audioURLLabel.lineBreakMode = .byTruncatingTail
|
|
|
|
|
|
|
|
stack.addArrangedSubview(title)
|
|
stack.addArrangedSubview(title)
|
|
|
stack.addArrangedSubview(dateLabel)
|
|
stack.addArrangedSubview(dateLabel)
|
|
|
- stack.addArrangedSubview(audioLabel)
|
|
|
|
|
|
|
+ stack.addArrangedSubview(audioButton)
|
|
|
|
|
+ stack.addArrangedSubview(audioURLLabel)
|
|
|
|
|
|
|
|
card.addSubview(stack)
|
|
card.addSubview(stack)
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
@@ -2039,6 +2082,11 @@ private extension ViewController {
|
|
|
return card
|
|
return card
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @objc private func aiCompanionAudioTapped(_ sender: NSButton) {
|
|
|
|
|
+ guard let url = aiCompanionAudioURLByView[ObjectIdentifier(sender)] else { return }
|
|
|
|
|
+ NSWorkspace.shared.open(url)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func mockAudioURLString(for meeting: ScheduledMeeting) -> String {
|
|
private func mockAudioURLString(for meeting: ScheduledMeeting) -> String {
|
|
|
let slug = meeting.id.replacingOccurrences(of: "[^A-Za-z0-9_-]", with: "-", options: .regularExpression)
|
|
let slug = meeting.id.replacingOccurrences(of: "[^A-Za-z0-9_-]", with: "-", options: .regularExpression)
|
|
|
return "https://mock-audio.local/\(slug).mp3"
|
|
return "https://mock-audio.local/\(slug).mp3"
|