|
@@ -1,4 +1,5 @@
|
|
|
import AppKit
|
|
import AppKit
|
|
|
|
|
+import SwiftUI
|
|
|
import WebKit
|
|
import WebKit
|
|
|
|
|
|
|
|
/// Titlebar icon control: white/neutral tint (no accent blue), subtle hover and selected backgrounds.
|
|
/// Titlebar icon control: white/neutral tint (no accent blue), subtle hover and selected backgrounds.
|
|
@@ -6,6 +7,10 @@ private final class TitlebarIconButton: NSButton {
|
|
|
private var isHovering = false
|
|
private var isHovering = false
|
|
|
private var hoverTrackingArea: NSTrackingArea?
|
|
private var hoverTrackingArea: NSTrackingArea?
|
|
|
var isToggleActive = false
|
|
var isToggleActive = false
|
|
|
|
|
+ /// Looks disabled (gray) but keeps `isEnabled == true` so the control still receives clicks (e.g. paywall).
|
|
|
|
|
+ var lockedForPaywallAppearance = false {
|
|
|
|
|
+ didSet { refreshVisuals() }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
override func awakeFromNib() {
|
|
override func awakeFromNib() {
|
|
|
super.awakeFromNib()
|
|
super.awakeFromNib()
|
|
@@ -69,7 +74,7 @@ private final class TitlebarIconButton: NSButton {
|
|
|
|
|
|
|
|
func refreshVisuals() {
|
|
func refreshVisuals() {
|
|
|
let white = NSColor.white
|
|
let white = NSColor.white
|
|
|
- if !isEnabled {
|
|
|
|
|
|
|
+ if !isEnabled || lockedForPaywallAppearance {
|
|
|
contentTintColor = white.withAlphaComponent(0.35)
|
|
contentTintColor = white.withAlphaComponent(0.35)
|
|
|
layer?.backgroundColor = NSColor.clear.cgColor
|
|
layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
return
|
|
return
|
|
@@ -159,6 +164,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
private var isGestureModeEnabled = true
|
|
private var isGestureModeEnabled = true
|
|
|
|
|
|
|
|
private var statusBarShortcutObserver: NSObjectProtocol?
|
|
private var statusBarShortcutObserver: NSObjectProtocol?
|
|
|
|
|
+ private var premiumSheetWindow: NSWindow?
|
|
|
|
|
|
|
|
private func setToggledStyle(_ button: TitlebarIconButton, isOn: Bool) {
|
|
private func setToggledStyle(_ button: TitlebarIconButton, isOn: Bool) {
|
|
|
button.setToggleActive(isOn)
|
|
button.setToggleActive(isOn)
|
|
@@ -490,7 +496,15 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func windowWillClose(_ notification: Notification) {
|
|
func windowWillClose(_ notification: Notification) {
|
|
|
- onClose?()
|
|
|
|
|
|
|
+ guard let w = notification.object as? NSWindow else { return }
|
|
|
|
|
+ if w === premiumSheetWindow {
|
|
|
|
|
+ premiumSheetWindow = nil
|
|
|
|
|
+ updateToolbarState()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if w === window {
|
|
|
|
|
+ onClose?()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func goBack() {
|
|
@objc private func goBack() {
|
|
@@ -521,10 +535,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
|
|
|
|
|
@objc private func toggleCurrentPageStatusBar() {
|
|
@objc private func toggleCurrentPageStatusBar() {
|
|
|
guard PremiumStore.shared.isPremiumUnlocked else {
|
|
guard PremiumStore.shared.isPremiumUnlocked else {
|
|
|
- showAlert(
|
|
|
|
|
- title: "Premium required",
|
|
|
|
|
- message: "Update to Premium to add shortcuts to the menu bar."
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ presentPremiumPaywall()
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
guard let app = resolvedLauncherAppForCurrentPage() else {
|
|
guard let app = resolvedLauncherAppForCurrentPage() else {
|
|
@@ -555,10 +566,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
|
|
|
|
|
@objc private func addCurrentPageToDesktop() {
|
|
@objc private func addCurrentPageToDesktop() {
|
|
|
guard PremiumStore.shared.isPremiumUnlocked else {
|
|
guard PremiumStore.shared.isPremiumUnlocked else {
|
|
|
- showAlert(
|
|
|
|
|
- title: "Premium required",
|
|
|
|
|
- message: "Update to Premium to save desktop shortcuts."
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ presentPremiumPaywall()
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
let url = currentPageURL()
|
|
let url = currentPageURL()
|
|
@@ -606,6 +614,17 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
|
|
|
|
|
private func updateAddToStatusBarButtonAppearance() {
|
|
private func updateAddToStatusBarButtonAppearance() {
|
|
|
let cfg = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
|
|
let cfg = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
|
|
|
|
|
+ guard PremiumStore.shared.isPremiumUnlocked else {
|
|
|
|
|
+ addToStatusBarButton.isEnabled = true
|
|
|
|
|
+ addToStatusBarButton.toolTip = "Add to Status Bar — Premium"
|
|
|
|
|
+ let img = NSImage(systemSymbolName: "arrow.up.square", accessibilityDescription: "Add to Status Bar")?
|
|
|
|
|
+ .withSymbolConfiguration(cfg)
|
|
|
|
|
+ addToStatusBarButton.image = img
|
|
|
|
|
+ addToStatusBarButton.image?.isTemplate = true
|
|
|
|
|
+ setToggledStyle(addToStatusBarButton, isOn: false)
|
|
|
|
|
+ addToStatusBarButton.refreshVisuals()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
guard let app = resolvedLauncherAppForCurrentPage() else {
|
|
guard let app = resolvedLauncherAppForCurrentPage() else {
|
|
|
addToStatusBarButton.isEnabled = false
|
|
addToStatusBarButton.isEnabled = false
|
|
|
addToStatusBarButton.toolTip = "Add to Status Bar — open this site from the launcher first"
|
|
addToStatusBarButton.toolTip = "Add to Status Bar — open this site from the launcher first"
|
|
@@ -652,6 +671,10 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func toggleGestureMode() {
|
|
@objc private func toggleGestureMode() {
|
|
|
|
|
+ guard PremiumStore.shared.isPremiumUnlocked else {
|
|
|
|
|
+ presentPremiumPaywall()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
isGestureModeEnabled.toggle()
|
|
isGestureModeEnabled.toggle()
|
|
|
webView.allowsBackForwardNavigationGestures = isGestureModeEnabled
|
|
webView.allowsBackForwardNavigationGestures = isGestureModeEnabled
|
|
|
if let button = handButton {
|
|
if let button = handButton {
|
|
@@ -664,6 +687,93 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// Borderless `NSWindow` is square; SwiftUI `.sheet` gets rounded chrome automatically — match that look.
|
|
|
|
|
+ private func configurePaywallWindowRoundedCorners(_ win: NSWindow) {
|
|
|
|
|
+ guard let cv = win.contentView else { return }
|
|
|
|
|
+ cv.wantsLayer = true
|
|
|
|
|
+ let r: CGFloat = 16
|
|
|
|
|
+ cv.layer?.cornerRadius = r
|
|
|
|
|
+ if #available(macOS 11.0, *) {
|
|
|
|
|
+ cv.layer?.cornerCurve = .continuous
|
|
|
|
|
+ }
|
|
|
|
|
+ cv.layer?.masksToBounds = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func presentPremiumPaywall() {
|
|
|
|
|
+ guard let parentWindow = window else { return }
|
|
|
|
|
+ if let existing = premiumSheetWindow, existing.isVisible {
|
|
|
|
|
+ existing.makeKeyAndOrderFront(nil)
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let paywallSize = NSSize(width: 540, height: 720)
|
|
|
|
|
+ let paywallWindow = NSWindow(
|
|
|
|
|
+ contentRect: NSRect(origin: .zero, size: paywallSize),
|
|
|
|
|
+ styleMask: [.borderless, .resizable],
|
|
|
|
|
+ backing: .buffered,
|
|
|
|
|
+ defer: false
|
|
|
|
|
+ )
|
|
|
|
|
+ paywallWindow.isReleasedWhenClosed = false
|
|
|
|
|
+ paywallWindow.delegate = self
|
|
|
|
|
+ paywallWindow.isMovableByWindowBackground = true
|
|
|
|
|
+ paywallWindow.hasShadow = true
|
|
|
|
|
+ paywallWindow.isOpaque = false
|
|
|
|
|
+ paywallWindow.backgroundColor = .clear
|
|
|
|
|
+
|
|
|
|
|
+ let hosting = NSHostingController(
|
|
|
|
|
+ rootView: PremiumFeaturesView(onDismissFromAppKitHost: { [weak self] in
|
|
|
|
|
+ self?.closePremiumPaywallWindow()
|
|
|
|
|
+ })
|
|
|
|
|
+ .environmentObject(PremiumStore.shared)
|
|
|
|
|
+ )
|
|
|
|
|
+ hosting.view.frame = NSRect(origin: .zero, size: paywallSize)
|
|
|
|
|
+ paywallWindow.contentViewController = hosting
|
|
|
|
|
+ configurePaywallWindowRoundedCorners(paywallWindow)
|
|
|
|
|
+
|
|
|
|
|
+ let pf = parentWindow.frame
|
|
|
|
|
+ paywallWindow.setFrame(
|
|
|
|
|
+ NSRect(
|
|
|
|
|
+ x: pf.midX - paywallSize.width / 2,
|
|
|
|
|
+ y: pf.midY - paywallSize.height / 2,
|
|
|
|
|
+ width: paywallSize.width,
|
|
|
|
|
+ height: paywallSize.height
|
|
|
|
|
+ ),
|
|
|
|
|
+ display: false
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ premiumSheetWindow = paywallWindow
|
|
|
|
|
+ parentWindow.addChildWindow(paywallWindow, ordered: .above)
|
|
|
|
|
+ paywallWindow.makeKeyAndOrderFront(nil)
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func closePremiumPaywallWindow() {
|
|
|
|
|
+ guard let w = premiumSheetWindow else { return }
|
|
|
|
|
+ w.parent?.removeChildWindow(w)
|
|
|
|
|
+ w.close()
|
|
|
|
|
+ // `premiumSheetWindow` and toolbar refresh happen in `windowWillClose`.
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Non‑Premium: only gesture, status bar, and desktop look locked; they stay enabled so taps open the paywall.
|
|
|
|
|
+ private func applyPremiumLockedTitlebarAppearance() {
|
|
|
|
|
+ let locked = !PremiumStore.shared.isPremiumUnlocked
|
|
|
|
|
+ handButton.lockedForPaywallAppearance = locked
|
|
|
|
|
+ addToDesktopButton.lockedForPaywallAppearance = locked
|
|
|
|
|
+ addToStatusBarButton.lockedForPaywallAppearance = locked
|
|
|
|
|
+ handButton.alphaValue = 1
|
|
|
|
|
+ addToDesktopButton.alphaValue = 1
|
|
|
|
|
+ addToStatusBarButton.alphaValue = 1
|
|
|
|
|
+ if locked {
|
|
|
|
|
+ handButton.isEnabled = true
|
|
|
|
|
+ addToDesktopButton.isEnabled = true
|
|
|
|
|
+ addToStatusBarButton.isEnabled = true
|
|
|
|
|
+ }
|
|
|
|
|
+ handButton.refreshVisuals()
|
|
|
|
|
+ addToDesktopButton.refreshVisuals()
|
|
|
|
|
+ addToStatusBarButton.refreshVisuals()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@objc private func openDownloads() {
|
|
@objc private func openDownloads() {
|
|
|
guard let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
|
|
guard let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
|
|
|
showAlert(title: "Unavailable", message: "Couldn’t find your Downloads folder.")
|
|
showAlert(title: "Unavailable", message: "Couldn’t find your Downloads folder.")
|
|
@@ -773,6 +883,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
// Sync center site name.
|
|
// Sync center site name.
|
|
|
updateSiteNameFromWebView()
|
|
updateSiteNameFromWebView()
|
|
|
updateAddToStatusBarButtonAppearance()
|
|
updateAddToStatusBarButtonAppearance()
|
|
|
|
|
+ applyPremiumLockedTitlebarAppearance()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func showAlert(title: String, message: String) {
|
|
private func showAlert(title: String, message: String) {
|