2026-06-11 18:12:13 -07:00
|
|
|
import AppKit
|
2026-06-29 23:42:39 -07:00
|
|
|
import IHatePDFsCore
|
2026-06-11 18:12:13 -07:00
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
@main
|
|
|
|
|
struct IHatePDFsApp: App {
|
2026-06-24 17:51:26 -07:00
|
|
|
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
|
|
|
|
|
2026-06-11 18:12:13 -07:00
|
|
|
var body: some Scene {
|
|
|
|
|
WindowGroup {
|
2026-06-18 16:44:19 -07:00
|
|
|
AppWindowRoot()
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
.windowStyle(.titleBar)
|
|
|
|
|
.commands {
|
2026-06-18 16:44:19 -07:00
|
|
|
AppCommands()
|
|
|
|
|
}
|
2026-06-24 17:51:26 -07:00
|
|
|
|
|
|
|
|
Settings {
|
|
|
|
|
SettingsView()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
private final class AppDelegate: NSObject, NSApplicationDelegate {
|
2026-06-29 23:42:39 -07:00
|
|
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
|
|
|
|
AppStateRegistry.shared.confirmApplicationShouldTerminate()
|
|
|
|
|
? .terminateNow
|
|
|
|
|
: .terminateCancel
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
private final class AppStateRegistry {
|
|
|
|
|
static let shared = AppStateRegistry()
|
|
|
|
|
|
|
|
|
|
private var appStates: [WeakAppState] = []
|
|
|
|
|
private(set) var isTerminationApproved = false
|
|
|
|
|
|
|
|
|
|
func register(_ appState: AppState) {
|
|
|
|
|
prune()
|
|
|
|
|
|
|
|
|
|
guard !appStates.contains(where: { $0.value === appState }) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appStates.append(WeakAppState(appState))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func unregister(_ appState: AppState) {
|
|
|
|
|
appStates.removeAll { $0.value == nil || $0.value === appState }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func confirmApplicationShouldTerminate() -> Bool {
|
|
|
|
|
prune()
|
|
|
|
|
|
|
|
|
|
for appState in appStates.compactMap(\.value) {
|
|
|
|
|
guard appState.confirmApplicationQuit() else {
|
|
|
|
|
cancelTerminationApproval()
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isTerminationApproved = true
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cancelTerminationApproval() {
|
|
|
|
|
isTerminationApproved = false
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
func closeOtherEmptyWindows(keeping activeAppState: AppState) {
|
|
|
|
|
prune()
|
|
|
|
|
|
|
|
|
|
for appState in appStates.compactMap(\.value) where appState !== activeAppState {
|
|
|
|
|
guard appState.document == nil,
|
|
|
|
|
!appState.hasUnsavedWork,
|
|
|
|
|
let window = appState.hostingWindow
|
|
|
|
|
else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 10:36:12 -07:00
|
|
|
func appStateForActiveWindow() -> AppState? {
|
|
|
|
|
prune()
|
|
|
|
|
|
|
|
|
|
let candidateWindows = [NSApp.keyWindow, NSApp.mainWindow].compactMap { $0 }
|
|
|
|
|
for window in candidateWindows {
|
|
|
|
|
if let appState = appStates.compactMap(\.value).first(where: { $0.hostingWindow === window }) {
|
|
|
|
|
return appState
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
private func prune() {
|
|
|
|
|
appStates.removeAll { $0.value == nil }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private final class WeakAppState {
|
|
|
|
|
weak var value: AppState?
|
|
|
|
|
|
|
|
|
|
init(_ value: AppState) {
|
|
|
|
|
self.value = value
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct AppWindowRoot: View {
|
|
|
|
|
@StateObject private var appState = AppState()
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
MainView()
|
|
|
|
|
.environmentObject(appState)
|
|
|
|
|
.focusedObject(appState)
|
2026-06-24 17:51:26 -07:00
|
|
|
.background(WindowCloseGuard(appState: appState))
|
2026-06-18 16:44:19 -07:00
|
|
|
.onOpenURL { url in
|
|
|
|
|
appState.loadDocument(from: url)
|
2026-06-29 23:42:39 -07:00
|
|
|
if appState.documentURL == url {
|
|
|
|
|
AppStateRegistry.shared.closeOtherEmptyWindows(keeping: appState)
|
|
|
|
|
}
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
2026-06-24 17:51:26 -07:00
|
|
|
.onAppear {
|
|
|
|
|
AppStateRegistry.shared.register(appState)
|
|
|
|
|
}
|
|
|
|
|
.onDisappear {
|
|
|
|
|
AppStateRegistry.shared.unregister(appState)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct WindowCloseGuard: NSViewRepresentable {
|
|
|
|
|
@ObservedObject var appState: AppState
|
|
|
|
|
|
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
|
|
|
Coordinator(appState: appState)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeNSView(context: Context) -> WindowCloseGuardView {
|
|
|
|
|
let view = WindowCloseGuardView()
|
|
|
|
|
view.onWindowChange = { [weak coordinator = context.coordinator] window in
|
|
|
|
|
coordinator?.attach(to: window)
|
|
|
|
|
}
|
|
|
|
|
return view
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateNSView(_ view: WindowCloseGuardView, context: Context) {
|
|
|
|
|
context.coordinator.appState = appState
|
|
|
|
|
context.coordinator.updateDocumentState()
|
|
|
|
|
view.onWindowChange = { [weak coordinator = context.coordinator] window in
|
|
|
|
|
coordinator?.attach(to: window)
|
|
|
|
|
}
|
|
|
|
|
view.reportWindow()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
final class Coordinator: NSObject, NSWindowDelegate {
|
|
|
|
|
weak var appState: AppState?
|
|
|
|
|
private weak var window: NSWindow?
|
|
|
|
|
private weak var previousDelegate: NSWindowDelegate?
|
|
|
|
|
|
|
|
|
|
init(appState: AppState) {
|
|
|
|
|
self.appState = appState
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func attach(to window: NSWindow?) {
|
|
|
|
|
guard self.window !== window else { return }
|
|
|
|
|
|
|
|
|
|
if let oldWindow = self.window, oldWindow.delegate === self {
|
|
|
|
|
oldWindow.delegate = previousDelegate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.window = window
|
|
|
|
|
previousDelegate = window?.delegate
|
2026-06-29 23:42:39 -07:00
|
|
|
appState?.hostingWindow = window
|
2026-06-24 17:51:26 -07:00
|
|
|
|
|
|
|
|
if window?.delegate !== self {
|
|
|
|
|
window?.delegate = self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateDocumentState()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateDocumentState() {
|
|
|
|
|
guard let window else { return }
|
|
|
|
|
|
|
|
|
|
let representedURL = appState?.documentURL
|
|
|
|
|
if window.representedURL != representedURL {
|
|
|
|
|
window.representedURL = representedURL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let isDocumentEdited = appState?.hasUnsavedWork == true
|
|
|
|
|
if window.isDocumentEdited != isDocumentEdited {
|
|
|
|
|
window.isDocumentEdited = isDocumentEdited
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
|
|
|
|
if previousDelegate?.windowShouldClose?(sender) == false {
|
|
|
|
|
AppStateRegistry.shared.cancelTerminationApproval()
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if AppStateRegistry.shared.isTerminationApproved {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return appState?.confirmDocumentWindowClose() ?? true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func windowWillClose(_ notification: Notification) {
|
|
|
|
|
previousDelegate?.windowWillClose?(notification)
|
|
|
|
|
|
|
|
|
|
if window?.delegate === self {
|
|
|
|
|
window?.delegate = previousDelegate
|
|
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
appState?.hostingWindow = nil
|
2026-06-24 17:51:26 -07:00
|
|
|
window = nil
|
|
|
|
|
previousDelegate = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
|
MainActor.assumeIsolated {
|
|
|
|
|
if window?.delegate === self {
|
|
|
|
|
window?.delegate = previousDelegate
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private final class WindowCloseGuardView: NSView {
|
|
|
|
|
var onWindowChange: ((NSWindow?) -> Void)?
|
|
|
|
|
|
|
|
|
|
override func viewDidMoveToWindow() {
|
|
|
|
|
super.viewDidMoveToWindow()
|
|
|
|
|
reportWindow()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func reportWindow() {
|
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
onWindowChange?(window)
|
|
|
|
|
}
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct AppCommands: Commands {
|
2026-06-30 10:36:12 -07:00
|
|
|
@FocusedObject private var focusedAppState: AppState?
|
|
|
|
|
|
|
|
|
|
private var appState: AppState? {
|
|
|
|
|
focusedAppState ?? AppStateRegistry.shared.appStateForActiveWindow()
|
|
|
|
|
}
|
2026-06-18 16:44:19 -07:00
|
|
|
|
|
|
|
|
private var hasDocument: Bool {
|
|
|
|
|
appState?.document != nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
private var hasTextSelection: Bool {
|
|
|
|
|
appState?.hasTextSelection == true
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
private var isHighlighterModeActive: Bool {
|
|
|
|
|
appState?.isHighlighterModeActive == true
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
private var canSaveDocument: Bool {
|
|
|
|
|
appState?.canSaveDocument == true
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 10:36:12 -07:00
|
|
|
private var canDeleteSelectedAnnotation: Bool {
|
|
|
|
|
appState?.canDeleteSelectedAnnotation == true
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
private var recentDocumentURLs: [URL] {
|
|
|
|
|
appState?.recentDocumentURLs ?? []
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
private var saveHelpText: String {
|
|
|
|
|
appState?.saveHelpText ?? "Open a PDF before saving."
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 16:44:19 -07:00
|
|
|
var body: some Commands {
|
|
|
|
|
CommandGroup(replacing: .newItem) {
|
|
|
|
|
Button("Open...") {
|
|
|
|
|
appState?.openDocument()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("o")
|
|
|
|
|
.disabled(appState == nil)
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
Menu("Open Recent") {
|
|
|
|
|
if recentDocumentURLs.isEmpty {
|
|
|
|
|
Button("No Recent PDFs") {}
|
|
|
|
|
.disabled(true)
|
|
|
|
|
} else {
|
|
|
|
|
ForEach(recentDocumentURLs, id: \.self) { url in
|
|
|
|
|
Button(url.lastPathComponent) {
|
|
|
|
|
appState?.openRecentDocument(url)
|
|
|
|
|
}
|
|
|
|
|
.help(url.path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
Button("Clear Menu") {
|
|
|
|
|
appState?.clearRecentDocuments()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.disabled(appState == nil)
|
|
|
|
|
|
2026-06-18 16:44:19 -07:00
|
|
|
Button("Save") {
|
|
|
|
|
appState?.saveDocument()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("s")
|
2026-06-24 17:51:26 -07:00
|
|
|
.disabled(!canSaveDocument)
|
|
|
|
|
.help(saveHelpText)
|
2026-06-18 16:44:19 -07:00
|
|
|
|
|
|
|
|
Button("Save As...") {
|
|
|
|
|
appState?.saveDocumentAs()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("s", modifiers: [.command, .shift])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Button("Share...") {
|
|
|
|
|
appState?.shareDocument()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("e", modifiers: [.command, .shift])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
2026-06-24 17:51:26 -07:00
|
|
|
Button("Settings...") {
|
|
|
|
|
openSettingsWindow()
|
|
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.keyboardShortcut(",")
|
2026-06-24 17:51:26 -07:00
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
Button(hasDocument ? "Close PDF" : "Close Window") {
|
|
|
|
|
if hasDocument {
|
|
|
|
|
appState?.closeDocument()
|
|
|
|
|
} else {
|
|
|
|
|
NSApp.keyWindow?.performClose(nil)
|
|
|
|
|
}
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
|
|
|
|
.keyboardShortcut("w")
|
2026-06-29 23:42:39 -07:00
|
|
|
.disabled(appState == nil)
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CommandGroup(after: .textEditing) {
|
|
|
|
|
Button("Find in PDF") {
|
|
|
|
|
appState?.showSearch()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("f")
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Button("Find Next") {
|
|
|
|
|
appState?.nextSearchResult()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("g")
|
|
|
|
|
.disabled(appState?.searchResults.isEmpty != false)
|
|
|
|
|
|
|
|
|
|
Button("Find Previous") {
|
|
|
|
|
appState?.previousSearchResult()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("g", modifiers: [.command, .shift])
|
|
|
|
|
.disabled(appState?.searchResults.isEmpty != false)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
CommandGroup(after: .toolbar) {
|
2026-06-18 16:44:19 -07:00
|
|
|
Button("Toggle Page Sidebar") {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState?.togglePageSidebar()
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
|
|
|
|
.keyboardShortcut("0", modifiers: [.command, .option])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
Button("Toggle Annotation List") {
|
|
|
|
|
appState?.toggleAnnotationSidebar()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("2", modifiers: [.command, .option])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Button("Toggle Right Sidebar") {
|
|
|
|
|
appState?.toggleRightSidebarVisibility()
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
|
|
|
|
.keyboardShortcut("1", modifiers: [.command, .option])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
Button("Zoom In") {
|
|
|
|
|
appState?.zoomIn()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("+")
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Button("Zoom Out") {
|
|
|
|
|
appState?.zoomOut()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("-")
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Button("Fit to Width") {
|
|
|
|
|
appState?.fitWidth()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("9", modifiers: [.command])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Button("Fit to Page") {
|
|
|
|
|
appState?.fitPage()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("8", modifiers: [.command])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Button("Two Pages Continuous") {
|
|
|
|
|
appState?.twoPageContinuous()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("7", modifiers: [.command])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CommandMenu("Annotate") {
|
2026-06-30 10:36:12 -07:00
|
|
|
Button("Undo Annotation Change") {
|
|
|
|
|
appState?.undoAnnotationChange()
|
|
|
|
|
}
|
|
|
|
|
.disabled(appState?.canUndoAnnotationChange != true)
|
|
|
|
|
|
|
|
|
|
Button("Redo Annotation Change") {
|
|
|
|
|
appState?.redoAnnotationChange()
|
|
|
|
|
}
|
|
|
|
|
.disabled(appState?.canRedoAnnotationChange != true)
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
Button("Cancel Annotation Mode") {
|
|
|
|
|
appState?.cancelActiveMode()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut(.cancelAction)
|
|
|
|
|
.disabled(appState?.canCancelActiveMode != true)
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
Button(isHighlighterModeActive ? "Turn Highlighter Off" : "Turn Highlighter On") {
|
|
|
|
|
appState?.toggleHighlighterMode()
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
|
|
|
|
.keyboardShortcut("h", modifiers: [.command, .shift])
|
2026-06-29 23:42:39 -07:00
|
|
|
.disabled(!hasDocument)
|
2026-06-18 16:44:19 -07:00
|
|
|
|
|
|
|
|
Button("Underline Selection") {
|
|
|
|
|
appState?.addUnderline()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("u", modifiers: [.command, .shift])
|
2026-06-24 17:51:26 -07:00
|
|
|
.disabled(!hasDocument || !hasTextSelection)
|
2026-06-18 16:44:19 -07:00
|
|
|
|
|
|
|
|
Button("Comment on Selection") {
|
|
|
|
|
appState?.addComment()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("n", modifiers: [.command, .shift])
|
2026-06-24 17:51:26 -07:00
|
|
|
.disabled(!hasDocument || !hasTextSelection)
|
2026-06-18 16:44:19 -07:00
|
|
|
|
|
|
|
|
Button("Add Free Text") {
|
|
|
|
|
appState?.addFreeText()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("t", modifiers: [.command, .shift])
|
|
|
|
|
.disabled(!hasDocument)
|
2026-06-30 10:36:12 -07:00
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
Button("Delete Selected Annotation") {
|
|
|
|
|
appState?.deleteSelectedAnnotation()
|
|
|
|
|
}
|
|
|
|
|
.disabled(!canDeleteSelectedAnnotation)
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
CommandMenu("Bookmark") {
|
|
|
|
|
Button(appState?.bookmarkActionTitle ?? "Add Bookmark") {
|
|
|
|
|
appState?.toggleBookmarkForCurrentPage()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("b", modifiers: [.command])
|
|
|
|
|
.disabled(!hasDocument)
|
|
|
|
|
|
|
|
|
|
Button("Go to Bookmark") {
|
|
|
|
|
appState?.goToSavedBookmark()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("b", modifiers: [.command, .option])
|
|
|
|
|
.disabled(appState?.savedBookmark == nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 16:44:19 -07:00
|
|
|
CommandGroup(after: .windowArrangement) {
|
|
|
|
|
Button("Minimize") {
|
|
|
|
|
appState?.minimizeWindow()
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("m", modifiers: [.command])
|
|
|
|
|
.disabled(appState == nil)
|
|
|
|
|
|
|
|
|
|
Button("Toggle Full Screen") {
|
|
|
|
|
appState?.toggleFullScreen()
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-18 16:44:19 -07:00
|
|
|
.keyboardShortcut("f", modifiers: [.command, .control])
|
|
|
|
|
.disabled(appState == nil)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-24 17:51:26 -07:00
|
|
|
|
|
|
|
|
private func openSettingsWindow() {
|
|
|
|
|
if !NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) {
|
|
|
|
|
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|