|
|
@@ -266,6 +266,7 @@ final class ViewController: NSViewController {
|
|
266
|
266
|
private var zoomJoinModeByView = [ObjectIdentifier: ZoomJoinMode]()
|
|
267
|
267
|
private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
|
|
268
|
268
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
|
269
|
+ private var aiCompanionAudioURLByView = [ObjectIdentifier: URL]()
|
|
269
|
270
|
private var paywallWindow: NSWindow?
|
|
270
|
271
|
private weak var paywallOverlayView: NSView?
|
|
271
|
272
|
private let paywallContentWidth: CGFloat = 520
|
|
|
@@ -1944,6 +1945,24 @@ private extension ViewController {
|
|
1944
|
1945
|
panel.translatesAutoresizingMaskIntoConstraints = false
|
|
1945
|
1946
|
panel.userInterfaceLayoutDirection = .leftToRight
|
|
1946
|
1947
|
|
|
|
1948
|
+ let scroll = NSScrollView()
|
|
|
1949
|
+ scroll.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1950
|
+ scroll.drawsBackground = false
|
|
|
1951
|
+ scroll.hasHorizontalScroller = false
|
|
|
1952
|
+ scroll.hasVerticalScroller = true
|
|
|
1953
|
+ scroll.autohidesScrollers = true
|
|
|
1954
|
+ scroll.borderType = .noBorder
|
|
|
1955
|
+ scroll.scrollerStyle = .overlay
|
|
|
1956
|
+ scroll.automaticallyAdjustsContentInsets = false
|
|
|
1957
|
+ let clip = TopAlignedClipView()
|
|
|
1958
|
+ clip.drawsBackground = false
|
|
|
1959
|
+ scroll.contentView = clip
|
|
|
1960
|
+ panel.addSubview(scroll)
|
|
|
1961
|
+
|
|
|
1962
|
+ let content = NSView()
|
|
|
1963
|
+ content.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1964
|
+ scroll.documentView = content
|
|
|
1965
|
+
|
|
1947
|
1966
|
let contentStack = NSStackView()
|
|
1948
|
1967
|
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
1949
|
1968
|
contentStack.userInterfaceLayoutDirection = .leftToRight
|
|
|
@@ -1987,12 +2006,23 @@ private extension ViewController {
|
|
1987
|
2006
|
}
|
|
1988
|
2007
|
}
|
|
1989
|
2008
|
|
|
1990
|
|
- panel.addSubview(contentStack)
|
|
|
2009
|
+ content.addSubview(contentStack)
|
|
1991
|
2010
|
NSLayoutConstraint.activate([
|
|
1992
|
|
- contentStack.leftAnchor.constraint(equalTo: panel.leftAnchor, constant: 28),
|
|
1993
|
|
- contentStack.rightAnchor.constraint(equalTo: panel.rightAnchor, constant: -28),
|
|
1994
|
|
- contentStack.topAnchor.constraint(equalTo: panel.topAnchor),
|
|
1995
|
|
- contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -16)
|
|
|
2011
|
+ scroll.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
|
|
|
2012
|
+ scroll.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
|
|
|
2013
|
+ scroll.topAnchor.constraint(equalTo: panel.topAnchor),
|
|
|
2014
|
+ scroll.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
|
|
|
2015
|
+
|
|
|
2016
|
+ content.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
|
|
|
2017
|
+ content.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
|
|
|
2018
|
+ content.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
|
|
|
2019
|
+ content.bottomAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.bottomAnchor),
|
|
|
2020
|
+ content.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor),
|
|
|
2021
|
+
|
|
|
2022
|
+ contentStack.leftAnchor.constraint(equalTo: content.leftAnchor, constant: 28),
|
|
|
2023
|
+ contentStack.rightAnchor.constraint(equalTo: content.rightAnchor, constant: -28),
|
|
|
2024
|
+ contentStack.topAnchor.constraint(equalTo: content.topAnchor),
|
|
|
2025
|
+ contentStack.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -16)
|
|
1996
|
2026
|
])
|
|
1997
|
2027
|
|
|
1998
|
2028
|
return panel
|
|
|
@@ -2019,14 +2049,27 @@ private extension ViewController {
|
|
2019
|
2049
|
dateLabel.alignment = .left
|
|
2020
|
2050
|
|
|
2021
|
2051
|
let audioLink = mockAudioURLString(for: meeting)
|
|
2022
|
|
- let audioLabel = textLabel("Mock Audio: \(audioLink)", font: typography.fieldLabel, color: palette.primaryBlue)
|
|
2023
|
|
- audioLabel.alignment = .left
|
|
2024
|
|
- audioLabel.maximumNumberOfLines = 2
|
|
2025
|
|
- audioLabel.lineBreakMode = .byTruncatingTail
|
|
|
2052
|
+ let audioButton = NSButton(title: "Play Audio", target: self, action: #selector(aiCompanionAudioTapped(_:)))
|
|
|
2053
|
+ audioButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
2054
|
+ audioButton.isBordered = false
|
|
|
2055
|
+ audioButton.bezelStyle = .inline
|
|
|
2056
|
+ audioButton.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
|
|
|
2057
|
+ audioButton.contentTintColor = palette.primaryBlue
|
|
|
2058
|
+ audioButton.alignment = .left
|
|
|
2059
|
+ audioButton.setButtonType(.momentaryPushIn)
|
|
|
2060
|
+ if let audioURL = URL(string: audioLink) {
|
|
|
2061
|
+ aiCompanionAudioURLByView[ObjectIdentifier(audioButton)] = audioURL
|
|
|
2062
|
+ }
|
|
|
2063
|
+
|
|
|
2064
|
+ let audioURLLabel = textLabel(audioLink, font: typography.fieldLabel, color: palette.textMuted)
|
|
|
2065
|
+ audioURLLabel.alignment = .left
|
|
|
2066
|
+ audioURLLabel.maximumNumberOfLines = 2
|
|
|
2067
|
+ audioURLLabel.lineBreakMode = .byTruncatingTail
|
|
2026
|
2068
|
|
|
2027
|
2069
|
stack.addArrangedSubview(title)
|
|
2028
|
2070
|
stack.addArrangedSubview(dateLabel)
|
|
2029
|
|
- stack.addArrangedSubview(audioLabel)
|
|
|
2071
|
+ stack.addArrangedSubview(audioButton)
|
|
|
2072
|
+ stack.addArrangedSubview(audioURLLabel)
|
|
2030
|
2073
|
|
|
2031
|
2074
|
card.addSubview(stack)
|
|
2032
|
2075
|
NSLayoutConstraint.activate([
|
|
|
@@ -2039,6 +2082,11 @@ private extension ViewController {
|
|
2039
|
2082
|
return card
|
|
2040
|
2083
|
}
|
|
2041
|
2084
|
|
|
|
2085
|
+ @objc private func aiCompanionAudioTapped(_ sender: NSButton) {
|
|
|
2086
|
+ guard let url = aiCompanionAudioURLByView[ObjectIdentifier(sender)] else { return }
|
|
|
2087
|
+ NSWorkspace.shared.open(url)
|
|
|
2088
|
+ }
|
|
|
2089
|
+
|
|
2042
|
2090
|
private func mockAudioURLString(for meeting: ScheduledMeeting) -> String {
|
|
2043
|
2091
|
let slug = meeting.id.replacingOccurrences(of: "[^A-Za-z0-9_-]", with: "-", options: .regularExpression)
|
|
2044
|
2092
|
return "https://mock-audio.local/\(slug).mp3"
|