aboutsummaryrefslogtreecommitdiff
path: root/Hotline/MacApp.swift
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-11-27 23:38:04 +0100
committerRuben Beltran del Rio <git@r.bdr.sh>2025-11-27 23:38:04 +0100
commit213710bf5bd6413c747bf126db50816ef5de5a6e (patch)
tree912f8cf87955a08077c92fea8ad934f50b7ab975 /Hotline/MacApp.swift
parentf466b21dc02f78c984ba6748e703f6780a7a0db4 (diff)
parent6a95b53616a4abfa306ddce43151cf4fefbd20ed (diff)
Merge remote-tracking branch 'upstream/main'
Diffstat (limited to 'Hotline/MacApp.swift')
-rw-r--r--Hotline/MacApp.swift371
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
+ }
+ }
+}
+