diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-11-27 23:38:04 +0100 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-11-27 23:38:04 +0100 |
| commit | 213710bf5bd6413c747bf126db50816ef5de5a6e (patch) | |
| tree | 912f8cf87955a08077c92fea8ad934f50b7ab975 /Hotline/MacApp.swift | |
| parent | f466b21dc02f78c984ba6748e703f6780a7a0db4 (diff) | |
| parent | 6a95b53616a4abfa306ddce43151cf4fefbd20ed (diff) | |
Merge remote-tracking branch 'upstream/main'
Diffstat (limited to 'Hotline/MacApp.swift')
| -rw-r--r-- | Hotline/MacApp.swift | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/Hotline/MacApp.swift b/Hotline/MacApp.swift new file mode 100644 index 0000000..454e9fd --- /dev/null +++ b/Hotline/MacApp.swift @@ -0,0 +1,371 @@ +import CloudKit +import SwiftData +import SwiftUI +import UniformTypeIdentifiers +import Darwin + +@Observable +final class AppLaunchState { + static let shared = AppLaunchState() + + enum LaunchState { + case loading + case launched + case terminated + } + + var launchState = LaunchState.loading +} + +class AppDelegate: NSObject, NSApplicationDelegate { + private var cloudKitObserverToken: Any? = nil + + func applicationDidFinishLaunching(_ notification: Notification) { + AppLaunchState.shared.launchState = .launched + + CKContainer.default().accountStatus { status, error in + switch status { + case .noAccount: + print("iCloud Unavailable") + + // We mark CloudKit has available now since we're not waiting on + // a server sync or anything. + AppState.shared.cloudKitReady = true + default: + print("iCloud Available") + + self.cloudKitObserverToken = NotificationCenter.default.addObserver( + forName: NSPersistentCloudKitContainer.eventChangedNotification, object: nil, + queue: OperationQueue.main + ) { [weak self] note in + print("iCloud Changed!") + AppState.shared.cloudKitReady = true + + guard let token = self?.cloudKitObserverToken else { return } + NotificationCenter.default.removeObserver(token) + } + } + } + + Task { + await AppUpdate.shared.checkForUpdatesOnLaunch() + } + } + + func applicationWillTerminate(_ notification: Notification) { + AppLaunchState.shared.launchState = .terminated + } +} + +@main +struct Application: App { + @Environment(\.scenePhase) private var scenePhase + @Environment(\.openWindow) private var openWindow + @Environment(\.openURL) private var openURL + + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + @State private var hotlinePanel: HotlinePanel? = nil + @State private var selection: TrackerSelection? = nil + @Bindable private var update = AppUpdate.shared + + @FocusedValue(\.activeHotlineModel) private var activeHotline: HotlineState? + @FocusedValue(\.activeServerState) private var activeServerState: ServerState? + + private var modelContainer: ModelContainer = { + let schema = Schema([ + Bookmark.self, + // ChatMessage.self + ]) + + // For records we want shared across devices. + let cloudKitConfiguration = ModelConfiguration( + schema: Schema([Bookmark.self]), + isStoredInMemoryOnly: false, + cloudKitDatabase: .private("iCloud.co.goodmake.hotline") + ) + + // For records we only need stored locally. +// let localConfiguration = ModelConfiguration( +// schema: Schema([ChatMessage.self]), +// isStoredInMemoryOnly: false +// ) + + let modelContainer = try! ModelContainer(for: schema, configurations: [cloudKitConfiguration]) + + // Print local SwiftData sqlite file. + // print(modelContainer.configurations.first?.url.path(percentEncoded: false)) + + return modelContainer + }() + + var body: some Scene { + // MARK: Tracker Window + Window("Servers", id: "servers") { + TrackerView(selection: $selection) + .frame(minWidth: 250, minHeight: 250) + } + .modelContainer(self.modelContainer) + .defaultSize(width: 700, height: 550) + .defaultPosition(.center) + .keyboardShortcut(.init("R"), modifiers: .command) + .onChange(of: AppLaunchState.shared.launchState) { + if AppLaunchState.shared.launchState == .launched { + if Prefs.shared.showBannerToolbar { + self.showBannerWindow() + } + } + } + .onChange(of: self.update.showWindow) { + if self.update.showWindow { + self.openWindow(id: "update") + } + } + + // MARK: About Box + Window("About", id: "about") { + AboutView() + .background(Color.hotlineRed, ignoresSafeAreaEdges: .all) + .windowFullScreenBehavior(.disabled) + .toolbar(removing: .title) + .gesture(WindowDragGesture()) + .background( + WindowConfigurator { window in + window.titlebarAppearsTransparent = true + window.titlebarSeparatorStyle = .none + window.isMovableByWindowBackground = true + + if let closeButton = window.standardWindowButton(.closeButton) { + closeButton.isHidden = false // make sure it’s visible + closeButton.isEnabled = true + } + + if let btn = window.standardWindowButton(.zoomButton) { + btn.isHidden = true + } + + if let btn = window.standardWindowButton(.miniaturizeButton) { + btn.isHidden = true + } + } + ) + } + .windowResizability(.contentSize) + .windowStyle(.hiddenTitleBar) + .restorationBehavior(.disabled) + .defaultPosition(.center) + .commandsRemoved() // Remove About that was automatically added to Window menu. + .commands { + CommandGroup(replacing: CommandGroupPlacement.appInfo) { + Button("About Hotline") { + openWindow(id: "about") + } + + Button("Check for Updates...") { + Task { + await AppUpdate.shared.checkForUpdatesManually() + } + } + } + } + + // MARK: Update Window + Window("New Update", id: "update") { + AppUpdateView() + .windowFullScreenBehavior(.disabled) + } + .windowResizability(.contentSize) + .windowStyle(.hiddenTitleBar) + .restorationBehavior(.disabled) + .defaultPosition(.center) + .commandsRemoved() + + // MARK: Server Window + WindowGroup(id: "server", for: Server.self) { server in + ServerView(server: server) + .frame(minWidth: 430, minHeight: 300) + } defaultValue: { + Server(name: nil, description: nil, address: "") + } + .windowToolbarStyle(.unified) +// .windowStyle(.hiddenTitleBar) + .defaultSize(width: 780, height: 640) + .defaultPosition(.center) + .modelContainer(self.modelContainer) + .onChange(of: activeServerState) { + AppState.shared.activeServerState = self.activeServerState + } + .onChange(of: activeHotline) { + AppState.shared.activeHotline = self.activeHotline + } + .commands { + CommandGroup(replacing: .newItem) { + Button("Connect to Server...") { + self.openWindow(id: "server") + } + .keyboardShortcut(.init("K"), modifiers: .command) + } + CommandGroup(before: .singleWindowList) { + Button("Toolbar") { + self.toggleBannerWindow() + } + .keyboardShortcut(.init("\\"), modifiers: [.shift, .command]) + } + CommandGroup(after: .help) { + Divider() + Button("Request Feature...") { + if let url = URL(string: "https://github.com/mierau/hotline/issues/new?labels=enhancement") { + self.openURL(url) + } + } + Button("Report Bug...") { + if let url = URL(string: "https://github.com/mierau/hotline/issues/new?labels=bug") { + self.openURL(url) + } + } + Divider() + Button("Open Latest Release Page...") { + if let url = URL(string: "https://github.com/mierau/hotline/releases/latest") { + self.openURL(url) + } + } + } + CommandMenu("Server") { + Button("Connect") { + guard let selection else { + return + } + self.connect(to: selection) + } + .disabled(selection == nil || selection?.server == nil) + .keyboardShortcut(.downArrow, modifiers: .command) + Button("Disconnect") { + if let hotline = activeHotline { + Task { + await hotline.disconnect() + } + } + } + .disabled(activeHotline?.status == .disconnected) + + Divider() + + Button("Broadcast Message...") { + activeServerState?.broadcastShown = true + } + .disabled(activeHotline?.access?.contains(.canBroadcast) != true) + .keyboardShortcut(.init("B"), modifiers: .command) + + Divider() + + Button("Chat") { + activeServerState?.selection = .chat + } + .disabled(activeHotline?.status != .loggedIn) + .keyboardShortcut(.init("1"), modifiers: .command) + Button("Board") { + activeServerState?.selection = .board + } + .disabled(activeHotline?.status != .loggedIn) + .keyboardShortcut(.init("2"), modifiers: .command) + Button("News") { + activeServerState?.selection = .news + } + .disabled(activeHotline?.status != .loggedIn || (activeHotline?.serverVersion ?? 0) < 151) + .keyboardShortcut(.init("3"), modifiers: .command) + Button("Files") { + activeServerState?.selection = .files + } + .disabled(activeHotline?.status != .loggedIn) + .keyboardShortcut(.init("4"), modifiers: .command) + + Divider() + + Button("Manage Accounts...") { + activeServerState?.accountsShown = true + } + .disabled(activeHotline?.status != .loggedIn || activeHotline?.access?.contains(.canOpenUsers) != true) + } + } + + // MARK: Settings Window + Settings { + SettingsView() + } + + // MARK: Transfers Window + Window("Transfers", id: "transfers") { + TransfersView() + .frame(minWidth: 500, minHeight: 200) + } + .defaultSize(width: 500, height: 400) + .defaultPosition(.topTrailing) + .keyboardShortcut(.init("T"), modifiers: [.shift, .command]) + + // MARK: Image Preview Window + WindowGroup(id: "preview-image", for: PreviewFileInfo.self) { $info in + FilePreviewImageView(info: $info) + } + .windowResizability(.contentSize) + .windowStyle(.titleBar) + .windowToolbarStyle(.unifiedCompact(showsTitle: true)) + .defaultSize(width: 350, height: 150) + .defaultPosition(.center) + .restorationBehavior(.disabled) + + // MARK: Text Preview Window + WindowGroup(id: "preview-text", for: PreviewFileInfo.self) { $info in + FilePreviewTextView(info: $info) + } + .windowResizability(.automatic) + .windowStyle(.titleBar) + .windowToolbarStyle(.unifiedCompact(showsTitle: true)) + .defaultSize(width: 450, height: 550) + .defaultPosition(.center) + .restorationBehavior(.disabled) + + // MARK: QuickLook Preview Window + WindowGroup(id: "preview-quicklook", for: PreviewFileInfo.self) { $info in + FilePreviewQuickLookView(info: $info) + } + .windowManagerRole(.associated) + .windowResizability(.automatic) + .windowStyle(.titleBar) + .windowToolbarStyle(.unifiedCompact(showsTitle: true)) + .defaultSize(width: 450, height: 550) + .defaultPosition(.center) + .restorationBehavior(.disabled) + } + + func connect(to item: TrackerSelection) { + if let server = item.server { + self.openWindow(id: "server", value: server) + } + } + + func showBannerWindow() { + if hotlinePanel == nil { + hotlinePanel = HotlinePanel(HotlinePanelView()) + } + + if hotlinePanel?.isVisible == false { + hotlinePanel?.orderFront(nil) + Prefs.shared.showBannerToolbar = true + } + } + + func toggleBannerWindow() { + if hotlinePanel == nil { + hotlinePanel = HotlinePanel(HotlinePanelView()) + } + + if hotlinePanel?.isVisible == true { + hotlinePanel?.orderOut(nil) + Prefs.shared.showBannerToolbar = false + } else { + hotlinePanel?.orderFront(nil) + Prefs.shared.showBannerToolbar = true + } + } +} + |