diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-05-01 21:22:18 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-05-01 21:32:15 -0700 |
| commit | 60a3f2d29dfff945f59787f5a7786c6e78ba7bb9 (patch) | |
| tree | e3d0aa6ae541790ea700353b0ac01fe8b4950f9f /Hotline | |
| parent | b9529a0d255ad41c62a4e3b93a47ebb5bb82a565 (diff) | |
New Hotline toolbar window for banner.
Diffstat (limited to 'Hotline')
| -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 |
14 files changed, 361 insertions, 51 deletions
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) |