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) } } } // Friendship Quest Remix /* 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.pizza.unlimited.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 } } }