|
|
@@ -53,7 +53,7 @@ final class ViewController: NSViewController {
|
|
|
private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
|
|
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
|
private weak var centeredTitleLabel: NSTextField?
|
|
|
- private weak var paywallWindow: NSWindow?
|
|
|
+ private var paywallWindow: NSWindow?
|
|
|
private let paywallContentWidth: CGFloat = 520
|
|
|
private var selectedPremiumPlan: PremiumPlan = .monthly
|
|
|
private var paywallPlanViews: [PremiumPlan: NSView] = [:]
|
|
|
@@ -445,8 +445,11 @@ private extension ViewController {
|
|
|
panel.title = "Get Premium"
|
|
|
panel.titleVisibility = .hidden
|
|
|
panel.titlebarAppearsTransparent = true
|
|
|
- panel.isFloatingPanel = true
|
|
|
- panel.hidesOnDeactivate = false
|
|
|
+ panel.isFloatingPanel = false
|
|
|
+ panel.level = .normal
|
|
|
+ panel.hidesOnDeactivate = true
|
|
|
+ panel.isReleasedWhenClosed = false
|
|
|
+ panel.delegate = self
|
|
|
panel.standardWindowButton(.closeButton)?.isHidden = true
|
|
|
panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
|
|
panel.standardWindowButton(.zoomButton)?.isHidden = true
|
|
|
@@ -457,9 +460,19 @@ private extension ViewController {
|
|
|
paywallWindow = panel
|
|
|
}
|
|
|
|
|
|
- @objc private func closePaywallClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
- paywallWindow?.close()
|
|
|
- paywallWindow = nil
|
|
|
+ @objc private func closePaywallClicked(_ sender: Any?) {
|
|
|
+ if let win = paywallWindow {
|
|
|
+ win.performClose(nil)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if let gesture = sender as? NSGestureRecognizer, let win = gesture.view?.window {
|
|
|
+ win.performClose(nil)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if let view = sender as? NSView, let win = view.window {
|
|
|
+ win.performClose(nil)
|
|
|
+ return
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
@@ -504,19 +517,22 @@ private extension ViewController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool) {
|
|
|
+ private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool, hovering: Bool = false) {
|
|
|
let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
|
|
|
let idleBorder = palette.inputBorder
|
|
|
+ let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
+ let hoverIdleBackground =
|
|
|
+ palette.sectionCard.blended(withFraction: 0.10, of: hoverBlend) ?? palette.sectionCard
|
|
|
let selectedBackground = darkModeEnabled
|
|
|
? NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
|
|
|
: NSColor(calibratedRed: 255.0 / 255.0, green: 246.0 / 255.0, blue: 236.0 / 255.0, alpha: 1)
|
|
|
- card.layer?.backgroundColor = (isSelected ? selectedBackground : palette.sectionCard).cgColor
|
|
|
- card.layer?.borderColor = (isSelected ? selectedBorder : idleBorder).cgColor
|
|
|
+ card.layer?.backgroundColor = (isSelected ? selectedBackground : (hovering ? hoverIdleBackground : palette.sectionCard)).cgColor
|
|
|
+ card.layer?.borderColor = (isSelected ? selectedBorder : (hovering ? selectedBorder.withAlphaComponent(0.55) : idleBorder)).cgColor
|
|
|
card.layer?.borderWidth = isSelected ? 2 : 1
|
|
|
card.layer?.shadowColor = NSColor.black.cgColor
|
|
|
- card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : 0.12
|
|
|
+ card.layer?.shadowOpacity = isSelected ? (darkModeEnabled ? 0.26 : 0.10) : (hovering ? 0.18 : 0.12)
|
|
|
card.layer?.shadowOffset = CGSize(width: 0, height: -1)
|
|
|
- card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : 5
|
|
|
+ card.layer?.shadowRadius = isSelected ? (darkModeEnabled ? 10 : 6) : (hovering ? 7 : 5)
|
|
|
}
|
|
|
|
|
|
private func viewForPage(_ page: SidebarPage) -> NSView {
|
|
|
@@ -1052,10 +1068,28 @@ private extension ViewController {
|
|
|
let topSpacer = NSView()
|
|
|
topSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
topRow.addArrangedSubview(topSpacer)
|
|
|
- let closeButton = iconRoundButton("✕", size: 28)
|
|
|
+ let closeButton = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
|
|
|
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ closeButton.isBordered = false
|
|
|
+ closeButton.bezelStyle = .regularSquare
|
|
|
+ closeButton.wantsLayer = true
|
|
|
+ closeButton.layer?.cornerRadius = 14
|
|
|
+ closeButton.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
+ closeButton.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
+ closeButton.layer?.borderWidth = 1
|
|
|
+ closeButton.font = typography.iconButton
|
|
|
+ closeButton.contentTintColor = palette.textSecondary
|
|
|
+ closeButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
+ closeButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
+ closeButton.onHoverChanged = { [weak closeButton, weak self] hovering in
|
|
|
+ guard let closeButton, let self else { return }
|
|
|
+ let base = self.palette.inputBackground
|
|
|
+ let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
+ let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
|
+ closeButton.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
|
+ closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
+ }
|
|
|
topRow.addArrangedSubview(closeButton)
|
|
|
- let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:)))
|
|
|
- closeButton.addGestureRecognizer(closeClick)
|
|
|
topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
contentStack.addArrangedSubview(topRow)
|
|
|
|
|
|
@@ -1125,8 +1159,11 @@ private extension ViewController {
|
|
|
contentStack.addArrangedSubview(offerWrap)
|
|
|
contentStack.setCustomSpacing(18, after: offerWrap)
|
|
|
|
|
|
- let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue)
|
|
|
+ let continueButton = HoverTrackingView()
|
|
|
continueButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ continueButton.wantsLayer = true
|
|
|
+ continueButton.layer?.cornerRadius = 14
|
|
|
+ continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
|
|
|
continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
|
continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
|
|
|
@@ -1136,6 +1173,13 @@ private extension ViewController {
|
|
|
continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
|
|
|
continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor)
|
|
|
])
|
|
|
+ let baseBlue = palette.primaryBlue
|
|
|
+ let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
+ let hoverBlue = baseBlue.blended(withFraction: 0.10, of: hoverBlend) ?? baseBlue
|
|
|
+ continueButton.onHoverChanged = { hovering in
|
|
|
+ continueButton.layer?.backgroundColor = (hovering ? hoverBlue : baseBlue).cgColor
|
|
|
+ }
|
|
|
+ continueButton.onHoverChanged?(false)
|
|
|
contentStack.addArrangedSubview(continueButton)
|
|
|
contentStack.setCustomSpacing(16, after: continueButton)
|
|
|
|
|
|
@@ -1175,7 +1219,7 @@ private extension ViewController {
|
|
|
plan: PremiumPlan,
|
|
|
strikePrice: String?
|
|
|
) -> NSView {
|
|
|
- let wrapper = NSView()
|
|
|
+ let wrapper = HoverTrackingView()
|
|
|
wrapper.translatesAutoresizingMaskIntoConstraints = false
|
|
|
wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
|
|
|
@@ -1249,6 +1293,11 @@ private extension ViewController {
|
|
|
wrapper.addGestureRecognizer(click)
|
|
|
premiumPlanByView[ObjectIdentifier(wrapper)] = plan
|
|
|
paywallPlanViews[plan] = card
|
|
|
+ wrapper.onHoverChanged = { [weak self, weak card] hovering in
|
|
|
+ guard let self, let card else { return }
|
|
|
+ self.applyPaywallPlanStyle(card, isSelected: plan == self.selectedPremiumPlan, hovering: hovering)
|
|
|
+ }
|
|
|
+ wrapper.onHoverChanged?(false)
|
|
|
|
|
|
return wrapper
|
|
|
}
|
|
|
@@ -1761,6 +1810,15 @@ extension ViewController: NSTextFieldDelegate {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+extension ViewController: NSWindowDelegate {
|
|
|
+ func windowWillClose(_ notification: Notification) {
|
|
|
+ guard let closingWindow = notification.object as? NSWindow else { return }
|
|
|
+ if closingWindow === paywallWindow {
|
|
|
+ paywallWindow = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
|
|
|
private class RowHitTestView: NSView {
|
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
|
@@ -1850,6 +1908,47 @@ private final class HoverSurfaceView: NSView {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private final class HoverButton: NSButton {
|
|
|
+ var onHoverChanged: ((Bool) -> Void)?
|
|
|
+ var showsHandCursor = true
|
|
|
+
|
|
|
+ private var trackingAreaRef: NSTrackingArea?
|
|
|
+ private var isHovering = false {
|
|
|
+ didSet {
|
|
|
+ guard isHovering != oldValue else { return }
|
|
|
+ onHoverChanged?(isHovering)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func updateTrackingAreas() {
|
|
|
+ super.updateTrackingAreas()
|
|
|
+ if let trackingAreaRef {
|
|
|
+ removeTrackingArea(trackingAreaRef)
|
|
|
+ }
|
|
|
+ let options: NSTrackingArea.Options = [
|
|
|
+ .activeInKeyWindow,
|
|
|
+ .inVisibleRect,
|
|
|
+ .mouseEnteredAndExited
|
|
|
+ ]
|
|
|
+ let tracking = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
|
|
|
+ addTrackingArea(tracking)
|
|
|
+ trackingAreaRef = tracking
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
+ super.mouseEntered(with: event)
|
|
|
+ if showsHandCursor {
|
|
|
+ NSCursor.pointingHand.set()
|
|
|
+ }
|
|
|
+ isHovering = true
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
+ super.mouseExited(with: event)
|
|
|
+ isHovering = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
private final class SettingsMenuViewController: NSViewController {
|
|
|
private let palette: Palette
|
|
|
private let typography: Typography
|