diff options
| -rw-r--r-- | Hotline.xcodeproj/project.pbxproj | 42 | ||||
| -rw-r--r-- | Hotline/Application-macOS.swift (renamed from Hotline/Application.swift) | 109 | ||||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Contents.json | 23 | ||||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png | bin | 0 -> 59733 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png | bin | 0 -> 207508 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png | bin | 0 -> 386312 bytes | |||
| -rw-r--r-- | Hotline/Models/ApplicationState.swift | 14 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 9 | ||||
| -rw-r--r-- | Hotline/Shared/HotlinePanel.swift | 48 | ||||
| -rw-r--r-- | Hotline/Shared/VisualEffectView.swift (renamed from Hotline/Utility/VisualEffectView.swift) | 17 | ||||
| -rw-r--r-- | Hotline/Sounds/Application-iOS.swift | 26 | ||||
| -rw-r--r-- | Hotline/Utility/SoundEffects.swift | 25 | ||||
| -rw-r--r-- | Hotline/macOS/ChatView.swift | 17 | ||||
| -rw-r--r-- | Hotline/macOS/HotlinePanelView.swift | 116 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 8 |
15 files changed, 395 insertions, 59 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj index 38c6cad..4ab9b1f 100644 --- a/Hotline.xcodeproj/project.pbxproj +++ b/Hotline.xcodeproj/project.pbxproj @@ -31,7 +31,9 @@ DA72A0E22B4DAA4000A0F48A /* NewsArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA72A0E12B4DAA4000A0F48A /* NewsArticle.swift */; }; DA77253F2B21176D006C5ABB /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA77253E2B21176D006C5ABB /* NewsView.swift */; platformFilter = ios; }; DA7725412B21435B006C5ABB /* ObservableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7725402B21435B006C5ABB /* ObservableScrollView.swift */; }; - DA9CAFBB2B126D5700CDA197 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBA2B126D5700CDA197 /* Application.swift */; }; + DA872B132BDDBF78008B1012 /* HotlinePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA872B122BDDBF78008B1012 /* HotlinePanel.swift */; }; + DA872B152BDDEE1A008B1012 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA872B142BDDEE1A008B1012 /* VisualEffectView.swift */; }; + DA9CAFBB2B126D5700CDA197 /* Application-macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBA2B126D5700CDA197 /* Application-macOS.swift */; }; DA9CAFBD2B126D5700CDA197 /* TrackerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBC2B126D5700CDA197 /* TrackerView.swift */; platformFilter = ios; }; DA9CAFC12B126D5800CDA197 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA9CAFC02B126D5800CDA197 /* Assets.xcassets */; }; DA9CAFC42B126D5800CDA197 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA9CAFC32B126D5800CDA197 /* Preview Assets.xcassets */; }; @@ -57,9 +59,12 @@ DABE8C062B57A06100884D28 /* Bookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABE8C052B57A06100884D28 /* Bookmarks.swift */; }; DABFCC292B1530DC009F40D2 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */; }; DAC002192B21630900A6C290 /* SwiftUIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */; }; + DAC3D97E2BC0F1ED00A727C9 /* Application-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */; }; + DAC3D9832BC33FD000A727C9 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */; }; DADDB28B2B22B31F0024040D /* Tracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28A2B22B31F0024040D /* Tracker.swift */; }; DADDB28D2B22B5920024040D /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28C2B22B5920024040D /* Server.swift */; }; DADDB28F2B238D850024040D /* Hotline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADDB28E2B238D850024040D /* Hotline.swift */; }; + DAE136BA2B9D1147007D8307 /* HotlinePanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE136B92B9D1147007D8307 /* HotlinePanelView.swift */; }; DAE734F92B2E4185000C56F6 /* ServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE734F82B2E4185000C56F6 /* ServerView.swift */; platformFilters = (macos, ); }; DAE734FB2B2E41F9000C56F6 /* TrackerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE734FA2B2E41F9000C56F6 /* TrackerView.swift */; platformFilters = (macos, ); }; DAE734FD2B2E65E9000C56F6 /* MessageBoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE734FC2B2E65E9000C56F6 /* MessageBoardView.swift */; platformFilters = (macos, ); }; @@ -68,7 +73,6 @@ DAE735032B30C0BB000C56F6 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE735022B30C0BB000C56F6 /* MessageView.swift */; platformFilters = (macos, ); }; DAE735052B3218D8000C56F6 /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE735042B3218D8000C56F6 /* NewsView.swift */; platformFilters = (macos, ); }; DAE735072B3251B3000C56F6 /* SoundEffects.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE735062B3251B3000C56F6 /* SoundEffects.swift */; }; - DAE735092B329810000C56F6 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE735082B329810000C56F6 /* VisualEffectView.swift */; platformFilters = (macos, ); }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -97,8 +101,10 @@ DA72A0E12B4DAA4000A0F48A /* NewsArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsArticle.swift; sourceTree = "<group>"; }; DA77253E2B21176D006C5ABB /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = "<group>"; }; DA7725402B21435B006C5ABB /* ObservableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableScrollView.swift; sourceTree = "<group>"; }; + DA872B122BDDBF78008B1012 /* HotlinePanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotlinePanel.swift; sourceTree = "<group>"; }; + DA872B142BDDEE1A008B1012 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = "<group>"; }; DA9CAFB72B126D5700CDA197 /* Hotline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hotline.app; sourceTree = BUILT_PRODUCTS_DIR; }; - DA9CAFBA2B126D5700CDA197 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; }; + DA9CAFBA2B126D5700CDA197 /* Application-macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Application-macOS.swift"; sourceTree = "<group>"; }; DA9CAFBC2B126D5700CDA197 /* TrackerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerView.swift; sourceTree = "<group>"; }; DA9CAFC02B126D5800CDA197 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; DA9CAFC32B126D5800CDA197 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; @@ -123,9 +129,12 @@ DABE8C052B57A06100884D28 /* Bookmarks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmarks.swift; sourceTree = "<group>"; }; DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = "<group>"; }; DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensions.swift; sourceTree = "<group>"; }; + DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Application-iOS.swift"; path = "Sounds/Application-iOS.swift"; sourceTree = "<group>"; }; + DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = "<group>"; }; DADDB28A2B22B31F0024040D /* Tracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tracker.swift; sourceTree = "<group>"; }; DADDB28C2B22B5920024040D /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; }; DADDB28E2B238D850024040D /* Hotline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hotline.swift; sourceTree = "<group>"; }; + DAE136B92B9D1147007D8307 /* HotlinePanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlinePanelView.swift; sourceTree = "<group>"; }; DAE734F82B2E4185000C56F6 /* ServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerView.swift; sourceTree = "<group>"; }; DAE734FA2B2E41F9000C56F6 /* TrackerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerView.swift; sourceTree = "<group>"; }; DAE734FC2B2E65E9000C56F6 /* MessageBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBoardView.swift; sourceTree = "<group>"; }; @@ -134,7 +143,6 @@ DAE735022B30C0BB000C56F6 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = "<group>"; }; DAE735042B3218D8000C56F6 /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = "<group>"; }; DAE735062B3251B3000C56F6 /* SoundEffects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundEffects.swift; sourceTree = "<group>"; }; - DAE735082B329810000C56F6 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -183,10 +191,12 @@ DA9CAFB92B126D5700CDA197 /* Hotline */ = { isa = PBXGroup; children = ( + DA9CAFBA2B126D5700CDA197 /* Application-macOS.swift */, + DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */, DABE8BF22B55DBFC00884D28 /* Sounds */, - DA9CAFBA2B126D5700CDA197 /* Application.swift */, DA4F2BF92B1A516F00D8ADDC /* iOS */, DAE734F72B2E4126000C56F6 /* macOS */, + DAC3D9812BC33F4400A727C9 /* Managers */, DADDB2892B22B2C60024040D /* Models */, DABFCC262B1530AE009F40D2 /* Hotline */, DAB4D87C2B4B783A0048A05C /* Shared */, @@ -215,6 +225,8 @@ DAB4D8832B4CABEF0048A05C /* DataAdditions.swift */, DAB4D87F2B4C8E9A0048A05C /* URLAdditions.swift */, DA4930BC2B4F8DD700822D0B /* NetSocket.swift */, + DA872B122BDDBF78008B1012 /* HotlinePanel.swift */, + DA872B142BDDEE1A008B1012 /* VisualEffectView.swift */, ); path = Shared; sourceTree = "<group>"; @@ -252,18 +264,25 @@ DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */, DA7725402B21435B006C5ABB /* ObservableScrollView.swift */, DAE735062B3251B3000C56F6 /* SoundEffects.swift */, - DAE735082B329810000C56F6 /* VisualEffectView.swift */, DA57536B2B36BA1D00FAC277 /* TextDocument.swift */, DABE8C032B57940A00884D28 /* DAKeychain.swift */, ); path = Utility; sourceTree = "<group>"; }; + DAC3D9812BC33F4400A727C9 /* Managers */ = { + isa = PBXGroup; + children = ( + ); + path = Managers; + sourceTree = "<group>"; + }; DADDB2892B22B2C60024040D /* Models */ = { isa = PBXGroup; children = ( 11A726072BE0672A000C1DA7 /* FileDetails.swift */, DADDB28A2B22B31F0024040D /* Tracker.swift */, + DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */, DABE8C052B57A06100884D28 /* Bookmarks.swift */, DA2863D92B37BF6E00A7D050 /* Preferences.swift */, DADDB28E2B238D850024040D /* Hotline.swift */, @@ -295,6 +314,7 @@ DAB4D87D2B4C8BCA0048A05C /* FilePreviewTextView.swift */, DA72A0DC2B4CD0BF00A0F48A /* NewsEditorView.swift */, DA2863D72B37AD1C00A7D050 /* SettingsView.swift */, + DAE136B92B9D1147007D8307 /* HotlinePanelView.swift */, ); path = macOS; sourceTree = "<group>"; @@ -389,10 +409,11 @@ DA6300972B24036B0034CBFD /* HotlineClient.swift in Sources */, DA43205E2B1D615600FC8843 /* ServerView.swift in Sources */, DA0D69912B1E894800C71DF5 /* FilesView.swift in Sources */, - DAE735092B329810000C56F6 /* VisualEffectView.swift in Sources */, + DAC3D9832BC33FD000A727C9 /* ApplicationState.swift in Sources */, DAB4D8802B4C8E9A0048A05C /* URLAdditions.swift in Sources */, DA2863DD2B3E8B7000A7D050 /* FilePreview.swift in Sources */, DA2863D82B37AD1C00A7D050 /* SettingsView.swift in Sources */, + DAC3D97E2BC0F1ED00A727C9 /* Application-iOS.swift in Sources */, DA7725412B21435B006C5ABB /* ObservableScrollView.swift in Sources */, DAE734FF2B2E6750000C56F6 /* ChatView.swift in Sources */, DA4930BD2B4F8DD700822D0B /* NetSocket.swift in Sources */, @@ -402,6 +423,7 @@ DABE8C042B57940B00884D28 /* DAKeychain.swift in Sources */, DA72A0E22B4DAA4000A0F48A /* NewsArticle.swift in Sources */, DAE734FB2B2E41F9000C56F6 /* TrackerView.swift in Sources */, + DAE136BA2B9D1147007D8307 /* HotlinePanelView.swift in Sources */, 11A726082BE0672A000C1DA7 /* FileDetails.swift in Sources */, DA9CAFBD2B126D5700CDA197 /* TrackerView.swift in Sources */, DAB4D87B2B4B78310048A05C /* AnimatedImageView.swift in Sources */, @@ -411,7 +433,7 @@ DA9CAFCB2B126E3300CDA197 /* HotlineTrackerClient.swift in Sources */, DA2863DA2B37BF6E00A7D050 /* Preferences.swift in Sources */, DA32CD4F2B2931CC0053B98B /* NewsInfo.swift in Sources */, - DA9CAFBB2B126D5700CDA197 /* Application.swift in Sources */, + DA9CAFBB2B126D5700CDA197 /* Application-macOS.swift in Sources */, DAAEE66B2B3FBC2100A5BA07 /* TransferInfo.swift in Sources */, DA0D698F2B1E841600C71DF5 /* MessageBoardView.swift in Sources */, DAE735072B3251B3000C56F6 /* SoundEffects.swift in Sources */, @@ -421,10 +443,12 @@ DABFCC292B1530DC009F40D2 /* FoundationExtensions.swift in Sources */, DA57536C2B36BA1D00FAC277 /* TextDocument.swift in Sources */, DA77253F2B21176D006C5ABB /* NewsView.swift in Sources */, + DA872B132BDDBF78008B1012 /* HotlinePanel.swift in Sources */, DA4F2BF82B16A17200D8ADDC /* HotlineProtocol.swift in Sources */, DA5753682B33E88A00FAC277 /* HotlineFileClient.swift in Sources */, DAB4D8822B4C8FED0048A05C /* FileIconView.swift in Sources */, DAAEE66F2B47625600A5BA07 /* FilePreviewImageView.swift in Sources */, + DA872B152BDDEE1A008B1012 /* VisualEffectView.swift in Sources */, DAAEE66D2B475F1400A5BA07 /* PreviewFileInfo.swift in Sources */, DAB4D8842B4CABEF0048A05C /* DataAdditions.swift in Sources */, DA0D698D2B1E7CF700C71DF5 /* UsersView.swift in Sources */, @@ -587,6 +611,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.3; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = co.goodmake.hotline; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -626,6 +651,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.3; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = co.goodmake.hotline; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Hotline/Application.swift b/Hotline/Application-macOS.swift index bf484ba..34fec3e 100644 --- a/Hotline/Application.swift +++ b/Hotline/Application-macOS.swift @@ -2,31 +2,48 @@ import SwiftUI import SwiftData import UniformTypeIdentifiers +#if os(macOS) + +@Observable +class AppLaunchState { + static let shared = AppLaunchState() + + enum LaunchState { + case loading + case launched + case terminated + } + + var launchState = LaunchState.loading +} + +class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + AppLaunchState.shared.launchState = .launched + } + + func applicationWillTerminate(_ notification: Notification) { + AppLaunchState.shared.launchState = .terminated + } +} + @main struct Application: App { - #if os(iOS) - private var model = Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient()) - #endif - - #if os(macOS) + @Environment(\.scenePhase) private var scenePhase @Environment(\.openWindow) private var openWindow @Environment(\.openURL) private var openURL - #endif + + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @State private var preferences = Prefs() @State private var soundEffects = SoundEffectPlayer() @State private var bookmarks = Bookmarks() - + @State private var hotlinePanel: HotlinePanel? = nil + @FocusedValue(\.activeHotlineModel) private var activeHotline: Hotline? @FocusedValue(\.activeServerState) private var activeServerState: ServerState? - + var body: some Scene { - #if os(iOS) - WindowGroup { - TrackerView() - .environment(model) - } - #elseif os(macOS) // MARK: Tracker Window Window("Servers", id: "servers") { TrackerView() @@ -36,7 +53,12 @@ struct Application: App { .keyboardShortcut(.init(.init("R"), modifiers: .command)) .defaultSize(width: 700, height: 550) .defaultPosition(.center) - + .onChange(of: AppLaunchState.shared.launchState) { + if AppLaunchState.shared.launchState == .launched { + showBannerWindow() + } + } + // MARK: Server Window WindowGroup(id: "server", for: Server.self) { server in ServerView(server: server) @@ -49,13 +71,41 @@ struct Application: App { } .defaultSize(width: 750, height: 700) .defaultPosition(.center) + .onChange(of: activeServerState) { + ApplicationState.shared.activeServerState = activeServerState + } + .onChange(of: activeHotline) { + ApplicationState.shared.activeHotline = activeHotline + } + .onChange(of: activeHotline?.serverTitle) { + if let hotline = activeHotline { + ApplicationState.shared.activeServerName = hotline.serverTitle + } + } + .onChange(of: activeHotline?.bannerImage) { + withAnimation { + ApplicationState.shared.activeServerBanner = activeHotline?.bannerImage + } + } + .onChange(of: activeHotline) { + ApplicationState.shared.activeHotline = activeHotline + if let hotline = activeHotline { + ApplicationState.shared.activeServerName = hotline.serverTitle + } + } .commands { - CommandGroup(replacing: CommandGroupPlacement.newItem) { + CommandGroup(replacing: .newItem) { Button("Connect to Server...") { openWindow(id: "server") } .keyboardShortcut(.init("K"), modifiers: .command) } + CommandGroup(after: .singleWindowList) { + Button("Toolbar") { + toggleBannerWindow() + } + .keyboardShortcut(.init("\\"), modifiers: [.shift, .command]) + } CommandGroup(after: .help) { Divider() Button("Request Feature...") { @@ -139,7 +189,30 @@ struct Application: App { .windowToolbarStyle(.unifiedCompact(showsTitle: true)) .defaultSize(width: 450, height: 550) .defaultPosition(.center) - - #endif + } + + func showBannerWindow() { + if hotlinePanel == nil { + hotlinePanel = HotlinePanel(HotlinePanelView()) + } + + if hotlinePanel?.isVisible == false { + hotlinePanel?.orderFront(nil) + } + } + + func toggleBannerWindow() { + if hotlinePanel == nil { + hotlinePanel = HotlinePanel(HotlinePanelView()) + } + + if hotlinePanel?.isVisible == true { + hotlinePanel?.orderOut(nil) + } + else { + hotlinePanel?.orderFront(nil) + } } } + +#endif diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Contents.json b/Hotline/Assets.xcassets/Default Banner.imageset/Contents.json new file mode 100644 index 0000000..1abbabf --- /dev/null +++ b/Hotline/Assets.xcassets/Default Banner.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Frame 2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png Binary files differnew file mode 100644 index 0000000..fae0905 --- /dev/null +++ b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png Binary files differnew file mode 100644 index 0000000..163e634 --- /dev/null +++ b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png Binary files differnew file mode 100644 index 0000000..3f2ef8f --- /dev/null +++ b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png diff --git a/Hotline/Models/ApplicationState.swift b/Hotline/Models/ApplicationState.swift new file mode 100644 index 0000000..124db6b --- /dev/null +++ b/Hotline/Models/ApplicationState.swift @@ -0,0 +1,14 @@ +import SwiftUI + +@Observable +class ApplicationState { + static let shared = ApplicationState() + + var activeHotline: Hotline? = nil + var activeServerState: ServerState? = nil + + // Frontmost server window information + var activeServerID: UUID? = nil + var activeServerBanner: NSImage? = nil + var activeServerName: String? = nil +} diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index f1c745f..3cc8366 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -1,10 +1,14 @@ import SwiftUI @Observable -final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { +class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate { + let id: UUID = UUID() let trackerClient: HotlineTrackerClient let client: HotlineClient - let soundEffects: SoundEffectPlayer = SoundEffectPlayer() + + static func == (lhs: Hotline, rhs: Hotline) -> Bool { + return lhs.id == rhs.id + } #if os(macOS) static func getClassicIcon(_ index: Int) -> NSImage? { @@ -178,7 +182,6 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { self.iconID = iconID self.client.login(address: server.address, port: server.port, login: server.login, password: server.password, username: username, iconID: UInt16(iconID)) { [weak self] err, serverName, serverVersion in - print("Server info:", serverName, serverVersion) self?.serverVersion = serverVersion ?? 123 if serverName != nil { self?.serverName = serverName diff --git a/Hotline/Shared/HotlinePanel.swift b/Hotline/Shared/HotlinePanel.swift new file mode 100644 index 0000000..60d1337 --- /dev/null +++ b/Hotline/Shared/HotlinePanel.swift @@ -0,0 +1,48 @@ +import Cocoa +import SwiftUI + +class HotlinePanel: NSPanel { + init(_ view: HotlinePanelView) { + super.init(contentRect: .zero, styleMask: [.nonactivatingPanel, .titled, .closable, .utilityWindow, .fullSizeContentView], backing: .buffered, defer: false) + + // Make sure that the panel is in front of almost all other windows + self.isFloatingPanel = false + self.level = .floating + self.hidesOnDeactivate = true + self.animationBehavior = .utilityWindow + + // Allow the panel to appear in a fullscreen space + self.collectionBehavior.insert(.fullScreenAuxiliary) + + // Don't delete panel state when it's closed. + self.isReleasedWhenClosed = false + + self.standardWindowButton(.closeButton)?.isHidden = true + self.standardWindowButton(.zoomButton)?.isHidden = true + self.standardWindowButton(.miniaturizeButton)?.isHidden = true + + // Make it transparent, the view inside will have to set the background. + // This is necessary because otherwise, we will have some space for the titlebar on top of the height of the view itself which we don't want. + self.isOpaque = false + self.backgroundColor = .clear + + // Since we don't show a statusbar, this allows us to drag the window by its background instead of the titlebar. + self.isMovableByWindowBackground = true + self.titlebarAppearsTransparent = true + + let hostingView = NSHostingView(rootView: view.edgesIgnoringSafeArea(.top)) + hostingView.sizingOptions = [.standardBounds] + + self.contentView = hostingView + + self.cascadeTopLeft(from: NSMakePoint(16, 16)) + } + + override var canBecomeKey: Bool { + return false + } + + override var canBecomeMain: Bool { + return false + } +} diff --git a/Hotline/Utility/VisualEffectView.swift b/Hotline/Shared/VisualEffectView.swift index 4f72a6e..e595c55 100644 --- a/Hotline/Utility/VisualEffectView.swift +++ b/Hotline/Shared/VisualEffectView.swift @@ -1,18 +1,12 @@ import SwiftUI -public struct VisualEffectView: NSViewRepresentable { +struct VisualEffectView: NSViewRepresentable +{ let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode - public init( - material: NSVisualEffectView.Material = .contentBackground, - blendingMode: NSVisualEffectView.BlendingMode = .withinWindow - ) { - self.material = material - self.blendingMode = blendingMode - } - - public func makeNSView(context: Context) -> NSVisualEffectView { + func makeNSView(context: Context) -> NSVisualEffectView + { let visualEffectView = NSVisualEffectView() visualEffectView.material = material visualEffectView.blendingMode = blendingMode @@ -20,7 +14,8 @@ public struct VisualEffectView: NSViewRepresentable { return visualEffectView } - public func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) { + func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) + { visualEffectView.material = material visualEffectView.blendingMode = blendingMode } diff --git a/Hotline/Sounds/Application-iOS.swift b/Hotline/Sounds/Application-iOS.swift new file mode 100644 index 0000000..5d9c56d --- /dev/null +++ b/Hotline/Sounds/Application-iOS.swift @@ -0,0 +1,26 @@ +import SwiftUI +import SwiftData +import UniformTypeIdentifiers + +#if os(iOS) + +@main +struct Application: App { + private var model = Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient()) + + @State private var preferences = Prefs() + @State private var soundEffects = SoundEffectPlayer() + @State private var bookmarks = Bookmarks() + + @FocusedValue(\.activeHotlineModel) private var activeHotline: Hotline? + @FocusedValue(\.activeServerState) private var activeServerState: ServerState? + + var body: some Scene { + WindowGroup { + TrackerView() + .environment(model) + } + } +} + +#endif diff --git a/Hotline/Utility/SoundEffects.swift b/Hotline/Utility/SoundEffects.swift index 175fa5b..931e27f 100644 --- a/Hotline/Utility/SoundEffects.swift +++ b/Hotline/Utility/SoundEffects.swift @@ -12,7 +12,9 @@ enum SoundEffects: String { @Observable class SoundEffectPlayer: NSObject, AVAudioPlayerDelegate { - var activeSounds: [AVAudioPlayer] = [] + static let shared = SoundEffectPlayer() + + private var activeSounds: [AVAudioPlayer] = [] func playSoundEffect(_ name: SoundEffects) { // Load a local sound file @@ -23,18 +25,21 @@ class SoundEffectPlayer: NSObject, AVAudioPlayerDelegate { return } - do { - let soundEffect = try AVAudioPlayer(contentsOf: soundFileURL) - soundEffect.delegate = self - - self.activeSounds.append(soundEffect) + DispatchQueue.main.async { [weak self] in + guard let self = self else { + return + } - soundEffect.volume = 0.75 - soundEffect.play() + if let soundEffect = try? AVAudioPlayer(contentsOf: soundFileURL) { + soundEffect.delegate = self + soundEffect.volume = 0.75 + soundEffect.play() + + self.activeSounds.append(soundEffect) + } } - catch {} } - + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { if let i = self.activeSounds.firstIndex(of: player) { self.activeSounds.remove(at: i) diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift index 7f7d677..1b82e02 100644 --- a/Hotline/macOS/ChatView.swift +++ b/Hotline/macOS/ChatView.swift @@ -38,21 +38,22 @@ struct ChatView: View { if msg.type == .agreement { VStack(alignment: .center, spacing: 16) { + #if os(iOS) if let bannerImage = self.model.bannerImage { - #if os(macOS) - Image(nsImage: bannerImage) - .resizable() - .scaledToFit() - .frame(maxWidth: 468.0) - .clipShape(RoundedRectangle(cornerRadius: 3)) - #elseif os(iOS) +// #if os(macOS) +// Image(nsImage: bannerImage) +// .resizable() +// .scaledToFit() +// .frame(maxWidth: 468.0) +// .clipShape(RoundedRectangle(cornerRadius: 3)) +// #elseif os(iOS) Image(uiImage: bannerImage) .resizable() .scaledToFit() .frame(maxWidth: 468.0) .clipShape(RoundedRectangle(cornerRadius: 3)) - #endif } + #endif VStack(spacing: 0) { ScrollView(.vertical) { diff --git a/Hotline/macOS/HotlinePanelView.swift b/Hotline/macOS/HotlinePanelView.swift new file mode 100644 index 0000000..36204a7 --- /dev/null +++ b/Hotline/macOS/HotlinePanelView.swift @@ -0,0 +1,116 @@ +import SwiftUI + +struct HotlinePanelView: View { + @Environment(\.openWindow) var openWindow + @Environment(\.colorScheme) var colorScheme + + @FocusedValue(\.activeHotlineModel) private var activeHotline: Hotline? + @FocusedValue(\.activeServerState) private var activeServerState: ServerState? + + var body: some View { + VStack { + Image(nsImage: ApplicationState.shared.activeServerBanner ?? NSImage(named: "Default Banner")!) + .interpolation(.high) + .resizable() + .scaledToFit() + .frame(width: 468, height: 60) + .clipShape(RoundedRectangle(cornerRadius: 6.0)) + .padding([.top, .leading, .trailing], 4) + + HStack(spacing: 16) { + Button { + openWindow(id: "servers") + } + label: { + Image(systemName: "globe.americas.fill") + .resizable() + .scaledToFit() + } + .buttonStyle(.plain) + .frame(width: 18, height: 18) + .help("Hotline Servers") + + Button { + ApplicationState.shared.activeServerState?.selection = .chat + } + label: { + Image(systemName: "bubble.fill") + .resizable() + .scaledToFit() + } + .buttonStyle(.plain) + .frame(width: 18, height: 18) + .opacity(ApplicationState.shared.activeServerState == nil ? 0.5 : 1.0) + .disabled(ApplicationState.shared.activeServerState == nil) + .help("Public Chat") + + Button { + ApplicationState.shared.activeServerState?.selection = .news + } + label: { + Image(systemName: "newspaper.fill") + .resizable() + .scaledToFit() + } + .buttonStyle(.plain) + .frame(width: 18, height: 18) + .opacity(ApplicationState.shared.activeServerState == nil ? 0.5 : 1.0) + .disabled(ApplicationState.shared.activeServerState == nil) + .help("News") + + Button { + ApplicationState.shared.activeServerState?.selection = .board + } + label: { + Image(systemName: "pin.fill") + .resizable() + .scaledToFit() + } + .buttonStyle(.plain) + .frame(width: 18, height: 18) + .opacity(ApplicationState.shared.activeServerState == nil ? 0.5 : 1.0) + .disabled(ApplicationState.shared.activeServerState == nil) + .help("Message Board") + + Button { + ApplicationState.shared.activeServerState?.selection = .files + } + label: { + Image(systemName: "folder.fill") + .resizable() + .scaledToFit() + } + .buttonStyle(.plain) + .frame(width: 18, height: 18) + .opacity(ApplicationState.shared.activeServerState == nil ? 0.5 : 1.0) + .disabled(ApplicationState.shared.activeServerState == nil) + .help("Files") + + Spacer() + + SettingsLink(label: { + Image(systemName: "gearshape.fill") + .resizable() + .scaledToFit() + }) + .buttonStyle(.plain) + .frame(width: 18, height: 18) + .help("Settings") + } + .padding(.top, 8) + .padding(.bottom, 16) + .padding([.leading, .trailing], 16) + } +// .frame(width: 468) +// .background(colorScheme == .dark ? .black : .white) + .background( + VisualEffectView(material: .headerView, blendingMode: .behindWindow) + .cornerRadius(10.0) + ) + } +} + +#Preview { + HotlinePanelView() + .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) +} diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift index 8176704..1892177 100644 --- a/Hotline/macOS/ServerView.swift +++ b/Hotline/macOS/ServerView.swift @@ -2,12 +2,17 @@ import SwiftUI import UniformTypeIdentifiers @Observable -class ServerState { +class ServerState: Equatable { + var id: UUID = UUID() var selection: ServerNavigationType init(selection: ServerNavigationType) { self.selection = selection } + + static func == (lhs: ServerState, rhs: ServerState) -> Bool { + return lhs.id == rhs.id + } } enum MenuItemType { @@ -121,6 +126,7 @@ struct ServerView: View { @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) private var colorScheme @Environment(\.controlActiveState) private var controlActiveState + @Environment(\.scenePhase) private var scenePhase @State private var model: Hotline = Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient()) @State private var state: ServerState = ServerState(selection: .chat) |