Sources/Orgstar/OrgstarApp.swift
106 lines · 3998 bytes
1import AppKit
2import OrgApp
3import SwiftUI
4
5/// Window-level requests the menu bar makes.
6@MainActor
7@Observable
8final class AppCommands {
9 var showingQuickOpen = false
10 var showingFolderPicker = false
11 var focusSearch = false
12 var showingConflict = false
13}
14
15@MainActor
16final class AppDelegate: NSObject, NSApplicationDelegate {
17 var session: DocumentSession?
18
19 func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
20
21 func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
22 guard let session else { return .terminateNow }
23 return UnsavedChanges.resolve(session) ? .terminateNow : .terminateCancel
24 }
25}
26
27/// Unsaved edits when leaving a file or quitting.
28@MainActor
29enum UnsavedChanges {
30 /// Whether the open file can be left: saved in automatic mode, or as the user chooses in
31 /// explicit mode. A save that fails or conflicts keeps the file open.
32 static func resolve(_ session: DocumentSession) -> Bool {
33 guard session.isDirty else { return true }
34 if session.saveMode == .automatic { return session.save() }
35 let alert = NSAlert()
36 alert.messageText = "Save changes to \(session.path.map { ($0 as NSString).lastPathComponent } ?? "this file")?"
37 alert.informativeText = "Your changes will be lost if you don't save them."
38 alert.addButton(withTitle: "Save")
39 alert.addButton(withTitle: "Don't Save")
40 alert.addButton(withTitle: "Cancel")
41 switch alert.runModal() {
42 case .alertFirstButtonReturn: return session.save()
43 case .alertSecondButtonReturn: return true
44 default: return false
45 }
46 }
47}
48
49@main
50struct OrgstarApp: App {
51 @NSApplicationDelegateAdaptor private var delegate: AppDelegate
52 @State private var workspace = Result { try WorkspaceModel(directory: WorkspaceModel.defaultDirectory) }
53 @State private var commands = AppCommands()
54 @State private var session = DocumentSession()
55
56 var body: some Scene {
57 Window("Orgstar", id: "main") {
58 switch workspace {
59 case .success(let model):
60 ContentView(workspace: model, session: session, commands: commands)
61 .onAppear { delegate.session = session }
62 case .failure(let error):
63 ContentUnavailableView("Can't open the index", systemImage: "exclamationmark.triangle", description: Text(error.localizedDescription))
64 }
65 }
66 .defaultSize(width: 1280, height: 860)
67 .commands {
68 CommandGroup(after: .newItem) {
69 Button("Add Folder…") { commands.showingFolderPicker = true }
70 .keyboardShortcut("o", modifiers: [.command, .shift])
71 Button("Quick Open…") { commands.showingQuickOpen = true }
72 .keyboardShortcut("p")
73 }
74 CommandGroup(replacing: .saveItem) {
75 Button("Save") {
76 if !session.save(), session.conflict != nil { commands.showingConflict = true }
77 }
78 .keyboardShortcut("s")
79 .disabled(session.path == nil || session.isReadOnly)
80 }
81 CommandGroup(after: .textEditing) {
82 Button("Search Notes") { commands.focusSearch = true }
83 .keyboardShortcut("f", modifiers: [.command, .shift])
84 }
85 }
86 Settings {
87 SettingsView()
88 }
89 }
90}
91
92struct SettingsView: View {
93 @AppStorage("saveMode") private var saveMode = SaveMode.automatic
94
95 var body: some View {
96 Form {
97 Picker("Save files:", selection: $saveMode) {
98 Text("Automatically, when typing stops").tag(SaveMode.automatic)
99 Text("Only with File ▸ Save (⌘S)").tag(SaveMode.explicit)
100 }
101 .pickerStyle(.radioGroup)
102 }
103 .padding(20)
104 .frame(width: 420)
105 }
106}