|
|
@@ -53,7 +53,7 @@ final class ViewController: NSViewController {
|
|
53
|
53
|
private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
|
|
54
|
54
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
55
|
55
|
private weak var centeredTitleLabel: NSTextField?
|
|
56
|
|
- private weak var paywallWindow: NSWindow?
|
|
|
56
|
+ private var paywallWindow: NSWindow?
|
|
57
|
57
|
private let paywallContentWidth: CGFloat = 520
|
|
58
|
58
|
private var selectedPremiumPlan: PremiumPlan = .monthly
|
|
59
|
59
|
private var paywallPlanViews: [PremiumPlan: NSView] = [:]
|
|
|
@@ -445,8 +445,11 @@ private extension ViewController {
|
|
445
|
445
|
panel.title = "Get Premium"
|
|
446
|
446
|
panel.titleVisibility = .hidden
|
|
447
|
447
|
panel.titlebarAppearsTransparent = true
|
|
448
|
|
- panel.isFloatingPanel = true
|
|
449
|
|
- panel.hidesOnDeactivate = false
|
|
|
448
|
+ panel.isFloatingPanel = false
|
|
|
449
|
+ panel.level = .normal
|
|
|
450
|
+ panel.hidesOnDeactivate = true
|
|
|
451
|
+ panel.isReleasedWhenClosed = false
|
|
|
452
|
+ panel.delegate = self
|
|
450
|
453
|
panel.standardWindowButton(.closeButton)?.isHidden = true
|
|
451
|
454
|
panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
|
452
|
455
|
panel.standardWindowButton(.zoomButton)?.isHidden = true
|
|
|
@@ -457,9 +460,19 @@ private extension ViewController {
|
|
457
|
460
|
paywallWindow = panel
|
|
458
|
461
|
}
|
|
459
|
462
|
|
|
460
|
|
- @objc private func closePaywallClicked(_ sender: NSClickGestureRecognizer) {
|
|
461
|
|
- paywallWindow?.close()
|
|
462
|
|
- paywallWindow = nil
|
|
|
463
|
+ @objc private func closePaywallClicked(_ sender: Any?) {
|
|
|
464
|
+ if let win = paywallWindow {
|
|
|
465
|
+ win.performClose(nil)
|
|
|
466
|
+ return
|
|
|
467
|
+ }
|
|
|
468
|
+ if let gesture = sender as? NSGestureRecognizer, let win = gesture.view?.window {
|
|
|
469
|
+ win.performClose(nil)
|
|
|
470
|
+ return
|
|
|
471
|
+ }
|
|
|
472
|
+ if let view = sender as? NSView, let win = view.window {
|
|
|
473
|
+ win.performClose(nil)
|
|
|
474
|
+ return
|
|
|
475
|
+ }
|
|
463
|
476
|
}
|
|
464
|
477
|
|
|
465
|
478
|
@objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
@@ -504,19 +517,22 @@ private extension ViewController {
|
|
504
|
517
|
}
|
|
505
|
518
|
}
|
|
506
|
519
|
|
|
507
|
|
- private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool) {
|
|
|
520
|
+ private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool, hovering: Bool = false) {
|
|
508
|
521
|
let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
|
|
509
|
522
|
let idleBorder = palette.inputBorder
|
|
|
523
|
+ let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
524
|
+ let hoverIdleBackground =
|
|
|
525
|
+ palette.sectionCard.blended(withFraction: 0.10, of: hoverBlend) ?? palette.sectionCard
|
|
510
|
526
|
let selectedBackground = darkModeEnabled
|
|
511
|
527
|
? NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
|
|
512
|
528
|
: NSColor(calibratedRed: 255.0 / 255.0, green: 246.0 / 255.0, blue: 236.0 / 255.0, alpha: 1)
|
|
513
|
|
- card.layer?.backgroundColor = (isSelected ? selectedBackground : palette.sectionCard).cgColor
|
|
514
|
|
- card.layer?.borderColor = (isSelected ? selectedBorder : idleBorder).cgColor
|
|
|
529
|
+ card.layer?.backgroundColor = (isSelected ? selectedBackground : (hovering ? hoverIdleBackground : palette.sectionCard)).cgColor
|
|
|
530
|
+ card.layer?.borderColor = (isSelected ? selectedBorder : (hovering ? selectedBorder.withAlphaComponent(0.55) : idleBorder)).cgColor
|
|
515
|
531
|
card.layer?.borderWidth = isSelected ? 2 : 1
|
|
516
|
532
|
card.layer?.shadowColor = NSColor.black.cgColor
|
|
517
|
|
- card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : 0.12
|
|
|
533
|
+ card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : (hovering ? 0.18 : 0.12)
|
|
518
|
534
|
card.layer?.shadowOffset = CGSize(width: 0, height: -1)
|
|
519
|
|
- card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : 5
|
|
|
535
|
+ card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : (hovering ? 7 : 5)
|
|
520
|
536
|
}
|
|
521
|
537
|
|
|
522
|
538
|
private func viewForPage(_ page: SidebarPage) -> NSView {
|
|
|
@@ -1052,10 +1068,28 @@ private extension ViewController {
|
|
1052
|
1068
|
let topSpacer = NSView()
|
|
1053
|
1069
|
topSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
1054
|
1070
|
topRow.addArrangedSubview(topSpacer)
|
|
1055
|
|
- let closeButton = iconRoundButton("✕", size: 28)
|
|
|
1071
|
+ let closeButton = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
|
|
|
1072
|
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1073
|
+ closeButton.isBordered = false
|
|
|
1074
|
+ closeButton.bezelStyle = .regularSquare
|
|
|
1075
|
+ closeButton.wantsLayer = true
|
|
|
1076
|
+ closeButton.layer?.cornerRadius = 14
|
|
|
1077
|
+ closeButton.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
1078
|
+ closeButton.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
1079
|
+ closeButton.layer?.borderWidth = 1
|
|
|
1080
|
+ closeButton.font = typography.iconButton
|
|
|
1081
|
+ closeButton.contentTintColor = palette.textSecondary
|
|
|
1082
|
+ closeButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
1083
|
+ closeButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
1084
|
+ closeButton.onHoverChanged = { [weak closeButton, weak self] hovering in
|
|
|
1085
|
+ guard let closeButton, let self else { return }
|
|
|
1086
|
+ let base = self.palette.inputBackground
|
|
|
1087
|
+ let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
1088
|
+ let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
|
1089
|
+ closeButton.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
|
1090
|
+ closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
1091
|
+ }
|
|
1056
|
1092
|
topRow.addArrangedSubview(closeButton)
|
|
1057
|
|
- let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:)))
|
|
1058
|
|
- closeButton.addGestureRecognizer(closeClick)
|
|
1059
|
1093
|
topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
1060
|
1094
|
contentStack.addArrangedSubview(topRow)
|
|
1061
|
1095
|
|
|
|
@@ -1125,8 +1159,11 @@ private extension ViewController {
|
|
1125
|
1159
|
contentStack.addArrangedSubview(offerWrap)
|
|
1126
|
1160
|
contentStack.setCustomSpacing(18, after: offerWrap)
|
|
1127
|
1161
|
|
|
1128
|
|
- let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue)
|
|
|
1162
|
+ let continueButton = HoverTrackingView()
|
|
1129
|
1163
|
continueButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1164
|
+ continueButton.wantsLayer = true
|
|
|
1165
|
+ continueButton.layer?.cornerRadius = 14
|
|
|
1166
|
+ continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
|
|
1130
|
1167
|
continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
1131
|
1168
|
continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
1132
|
1169
|
styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
|
|
|
@@ -1136,6 +1173,13 @@ private extension ViewController {
|
|
1136
|
1173
|
continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
|
|
1137
|
1174
|
continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor)
|
|
1138
|
1175
|
])
|
|
|
1176
|
+ let baseBlue = palette.primaryBlue
|
|
|
1177
|
+ let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
1178
|
+ let hoverBlue = baseBlue.blended(withFraction: 0.10, of: hoverBlend) ?? baseBlue
|
|
|
1179
|
+ continueButton.onHoverChanged = { hovering in
|
|
|
1180
|
+ continueButton.layer?.backgroundColor = (hovering ? hoverBlue : baseBlue).cgColor
|
|
|
1181
|
+ }
|
|
|
1182
|
+ continueButton.onHoverChanged?(false)
|
|
1139
|
1183
|
contentStack.addArrangedSubview(continueButton)
|
|
1140
|
1184
|
contentStack.setCustomSpacing(16, after: continueButton)
|
|
1141
|
1185
|
|
|
|
@@ -1175,7 +1219,7 @@ private extension ViewController {
|
|
1175
|
1219
|
plan: PremiumPlan,
|
|
1176
|
1220
|
strikePrice: String?
|
|
1177
|
1221
|
) -> NSView {
|
|
1178
|
|
- let wrapper = NSView()
|
|
|
1222
|
+ let wrapper = HoverTrackingView()
|
|
1179
|
1223
|
wrapper.translatesAutoresizingMaskIntoConstraints = false
|
|
1180
|
1224
|
wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
1181
|
1225
|
wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
|
|
|
@@ -1249,6 +1293,11 @@ private extension ViewController {
|
|
1249
|
1293
|
wrapper.addGestureRecognizer(click)
|
|
1250
|
1294
|
premiumPlanByView[ObjectIdentifier(wrapper)] = plan
|
|
1251
|
1295
|
paywallPlanViews[plan] = card
|
|
|
1296
|
+ wrapper.onHoverChanged = { [weak self, weak card] hovering in
|
|
|
1297
|
+ guard let self, let card else { return }
|
|
|
1298
|
+ self.applyPaywallPlanStyle(card, isSelected: plan == self.selectedPremiumPlan, hovering: hovering)
|
|
|
1299
|
+ }
|
|
|
1300
|
+ wrapper.onHoverChanged?(false)
|
|
1252
|
1301
|
|
|
1253
|
1302
|
return wrapper
|
|
1254
|
1303
|
}
|
|
|
@@ -1761,6 +1810,15 @@ extension ViewController: NSTextFieldDelegate {
|
|
1761
|
1810
|
}
|
|
1762
|
1811
|
}
|
|
1763
|
1812
|
|
|
|
1813
|
+extension ViewController: NSWindowDelegate {
|
|
|
1814
|
+ func windowWillClose(_ notification: Notification) {
|
|
|
1815
|
+ guard let closingWindow = notification.object as? NSWindow else { return }
|
|
|
1816
|
+ if closingWindow === paywallWindow {
|
|
|
1817
|
+ paywallWindow = nil
|
|
|
1818
|
+ }
|
|
|
1819
|
+ }
|
|
|
1820
|
+}
|
|
|
1821
|
+
|
|
1764
|
1822
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
1765
|
1823
|
private class RowHitTestView: NSView {
|
|
1766
|
1824
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
|
@@ -1850,6 +1908,47 @@ private final class HoverSurfaceView: NSView {
|
|
1850
|
1908
|
}
|
|
1851
|
1909
|
}
|
|
1852
|
1910
|
|
|
|
1911
|
+private final class HoverButton: NSButton {
|
|
|
1912
|
+ var onHoverChanged: ((Bool) -> Void)?
|
|
|
1913
|
+ var showsHandCursor = true
|
|
|
1914
|
+
|
|
|
1915
|
+ private var trackingAreaRef: NSTrackingArea?
|
|
|
1916
|
+ private var isHovering = false {
|
|
|
1917
|
+ didSet {
|
|
|
1918
|
+ guard isHovering != oldValue else { return }
|
|
|
1919
|
+ onHoverChanged?(isHovering)
|
|
|
1920
|
+ }
|
|
|
1921
|
+ }
|
|
|
1922
|
+
|
|
|
1923
|
+ override func updateTrackingAreas() {
|
|
|
1924
|
+ super.updateTrackingAreas()
|
|
|
1925
|
+ if let trackingAreaRef {
|
|
|
1926
|
+ removeTrackingArea(trackingAreaRef)
|
|
|
1927
|
+ }
|
|
|
1928
|
+ let options: NSTrackingArea.Options = [
|
|
|
1929
|
+ .activeInKeyWindow,
|
|
|
1930
|
+ .inVisibleRect,
|
|
|
1931
|
+ .mouseEnteredAndExited
|
|
|
1932
|
+ ]
|
|
|
1933
|
+ let tracking = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
|
|
|
1934
|
+ addTrackingArea(tracking)
|
|
|
1935
|
+ trackingAreaRef = tracking
|
|
|
1936
|
+ }
|
|
|
1937
|
+
|
|
|
1938
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
1939
|
+ super.mouseEntered(with: event)
|
|
|
1940
|
+ if showsHandCursor {
|
|
|
1941
|
+ NSCursor.pointingHand.set()
|
|
|
1942
|
+ }
|
|
|
1943
|
+ isHovering = true
|
|
|
1944
|
+ }
|
|
|
1945
|
+
|
|
|
1946
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
1947
|
+ super.mouseExited(with: event)
|
|
|
1948
|
+ isHovering = false
|
|
|
1949
|
+ }
|
|
|
1950
|
+}
|
|
|
1951
|
+
|
|
1853
|
1952
|
private final class SettingsMenuViewController: NSViewController {
|
|
1854
|
1953
|
private let palette: Palette
|
|
1855
|
1954
|
private let typography: Typography
|