aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
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.json23
-rw-r--r--Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.pngbin0 -> 59733 bytes
-rw-r--r--Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.pngbin0 -> 207508 bytes
-rw-r--r--Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.pngbin0 -> 386312 bytes
-rw-r--r--Hotline/Models/ApplicationState.swift14
-rw-r--r--Hotline/Models/Hotline.swift9
-rw-r--r--Hotline/Shared/HotlinePanel.swift48
-rw-r--r--Hotline/Shared/VisualEffectView.swift (renamed from Hotline/Utility/VisualEffectView.swift)17
-rw-r--r--Hotline/Sounds/Application-iOS.swift26
-rw-r--r--Hotline/Utility/SoundEffects.swift25
-rw-r--r--Hotline/macOS/ChatView.swift17
-rw-r--r--Hotline/macOS/HotlinePanelView.swift116
-rw-r--r--Hotline/macOS/ServerView.swift8
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
new file mode 100644
index 0000000..fae0905
--- /dev/null
+++ b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png
new file mode 100644
index 0000000..163e634
--- /dev/null
+++ b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@2x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png
new file mode 100644
index 0000000..3f2ef8f
--- /dev/null
+++ b/Hotline/Assets.xcassets/Default Banner.imageset/Frame 2@3x.png
Binary files differ
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)