aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-10-27 21:48:20 -0700
committerDustin Mierau <dustin@mierau.me>2025-10-27 21:48:20 -0700
commit4bb0ffba596e41a8309ba50d222248a0ba3eb42e (patch)
treefcd1c2b11c071a624d0b3b56ed16340272200c7d /Hotline
parent924012e6ee03fff5570e9362e29be9b04816b777 (diff)
Random project housekeeping/organizing source files.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/macOS/Accounts/AccountManagerView.swift (renamed from Hotline/macOS/AccountManagerView.swift)0
-rw-r--r--Hotline/macOS/Board/MessageBoardEditorView.swift (renamed from Hotline/macOS/MessageBoardEditorView.swift)0
-rw-r--r--Hotline/macOS/Board/MessageBoardView.swift (renamed from Hotline/macOS/MessageBoardView.swift)0
-rw-r--r--Hotline/macOS/Chat/ChatView.swift (renamed from Hotline/macOS/ChatView.swift)0
-rw-r--r--Hotline/macOS/Chat/ServerAgreementView.swift (renamed from Hotline/macOS/ServerAgreementView.swift)0
-rw-r--r--Hotline/macOS/Chat/ServerMessageView.swift (renamed from Hotline/macOS/ServerMessageView.swift)0
-rw-r--r--Hotline/macOS/Files/FileDetailsView.swift (renamed from Hotline/macOS/FileDetailsView.swift)8
-rw-r--r--Hotline/macOS/Files/FileItemView.swift67
-rw-r--r--Hotline/macOS/Files/FilePreviewImageView.swift (renamed from Hotline/macOS/FilePreviewImageView.swift)0
-rw-r--r--Hotline/macOS/Files/FilePreviewTextView.swift (renamed from Hotline/macOS/FilePreviewTextView.swift)0
-rw-r--r--Hotline/macOS/Files/FilesView.swift (renamed from Hotline/macOS/FilesView.swift)205
-rw-r--r--Hotline/macOS/Files/FolderItemView.swift140
-rw-r--r--Hotline/macOS/News/NewsEditorView.swift (renamed from Hotline/macOS/NewsEditorView.swift)0
-rw-r--r--Hotline/macOS/News/NewsItemView.swift (renamed from Hotline/macOS/NewsItemView.swift)0
-rw-r--r--Hotline/macOS/News/NewsView.swift (renamed from Hotline/macOS/NewsView.swift)0
-rw-r--r--Hotline/macOS/Settings/GeneralSettingsView.swift67
-rw-r--r--Hotline/macOS/Settings/IconSettingsView.swift58
-rw-r--r--Hotline/macOS/Settings/SettingsView.swift31
-rw-r--r--Hotline/macOS/Settings/SoundSettingsView.swift40
-rw-r--r--Hotline/macOS/SettingsView.swift193
-rw-r--r--Hotline/macOS/TrackerView.swift64
21 files changed, 420 insertions, 453 deletions
diff --git a/Hotline/macOS/AccountManagerView.swift b/Hotline/macOS/Accounts/AccountManagerView.swift
index 57682cc..57682cc 100644
--- a/Hotline/macOS/AccountManagerView.swift
+++ b/Hotline/macOS/Accounts/AccountManagerView.swift
diff --git a/Hotline/macOS/MessageBoardEditorView.swift b/Hotline/macOS/Board/MessageBoardEditorView.swift
index 474384e..474384e 100644
--- a/Hotline/macOS/MessageBoardEditorView.swift
+++ b/Hotline/macOS/Board/MessageBoardEditorView.swift
diff --git a/Hotline/macOS/MessageBoardView.swift b/Hotline/macOS/Board/MessageBoardView.swift
index f870b0c..f870b0c 100644
--- a/Hotline/macOS/MessageBoardView.swift
+++ b/Hotline/macOS/Board/MessageBoardView.swift
diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/Chat/ChatView.swift
index 65087c6..65087c6 100644
--- a/Hotline/macOS/ChatView.swift
+++ b/Hotline/macOS/Chat/ChatView.swift
diff --git a/Hotline/macOS/ServerAgreementView.swift b/Hotline/macOS/Chat/ServerAgreementView.swift
index e72de7d..e72de7d 100644
--- a/Hotline/macOS/ServerAgreementView.swift
+++ b/Hotline/macOS/Chat/ServerAgreementView.swift
diff --git a/Hotline/macOS/ServerMessageView.swift b/Hotline/macOS/Chat/ServerMessageView.swift
index 6c3c60e..6c3c60e 100644
--- a/Hotline/macOS/ServerMessageView.swift
+++ b/Hotline/macOS/Chat/ServerMessageView.swift
diff --git a/Hotline/macOS/FileDetailsView.swift b/Hotline/macOS/Files/FileDetailsView.swift
index 34e083b..812f4e5 100644
--- a/Hotline/macOS/FileDetailsView.swift
+++ b/Hotline/macOS/Files/FileDetailsView.swift
@@ -112,15 +112,15 @@ struct FileDetailsView: View {
static let byteFormatter = ByteCountFormatter()
private func formattedFileSize(_ fileSize: UInt) -> String {
- FileView.byteFormatter.allowedUnits = [.useAll]
- FileView.byteFormatter.countStyle = .file
- return FileView.byteFormatter.string(fromByteCount: Int64(fileSize))
+ FileItemView.byteFormatter.allowedUnits = [.useAll]
+ FileItemView.byteFormatter.countStyle = .file
+ return FileItemView.byteFormatter.string(fromByteCount: Int64(fileSize))
}
// Format byte count Int into string like: 23.4M (24,601,664 bytes)
private func formattedSize(byteCount: Int) -> String {
let formattedByteCount = FileDetailsView.byteCountSizeFormatter.string(from: NSNumber(value:byteCount)) ?? "0"
- return "\(FileView.byteFormatter.string(fromByteCount: Int64(byteCount))) (\(formattedByteCount) bytes)"
+ return "\(FileItemView.byteFormatter.string(fromByteCount: Int64(byteCount))) (\(formattedByteCount) bytes)"
}
private func isEdited() -> Bool {
diff --git a/Hotline/macOS/Files/FileItemView.swift b/Hotline/macOS/Files/FileItemView.swift
new file mode 100644
index 0000000..5da744f
--- /dev/null
+++ b/Hotline/macOS/Files/FileItemView.swift
@@ -0,0 +1,67 @@
+import SwiftUI
+
+struct FileItemView: View {
+ @Environment(Hotline.self) private var model: Hotline
+
+ var file: FileInfo
+ let depth: Int
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 0) {
+ Spacer()
+ .frame(width: CGFloat(depth * (12 + 2)))
+
+ Spacer()
+ .frame(width: 10)
+ .padding(.leading, 4)
+ .padding(.trailing, 8)
+
+ HStack(alignment: .center) {
+ if file.isUnavailable {
+ Image(systemName: "questionmark.app.fill")
+ .frame(width: 16, height: 16)
+ .opacity(0.5)
+ }
+ else {
+ FileIconView(filename: file.name, fileType: file.type)
+ .frame(width: 16, height: 16)
+ }
+ }
+ .frame(width: 16)
+ .padding(.trailing, 6)
+
+ Text(file.name)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .opacity(file.isUnavailable ? 0.5 : 1.0)
+
+ Spacer()
+ if !file.isUnavailable {
+ Text(formattedFileSize(file.fileSize))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .padding(.trailing, 6)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+
+ if file.expanded {
+ ForEach(file.children!, id: \.self) { childFile in
+ if childFile.isFolder {
+ FolderItemView(file: childFile, depth: self.depth + 1).tag(file.id)
+ }
+ else {
+ FileItemView(file: childFile, depth: self.depth + 1).tag(file.id)
+ }
+ }
+ }
+ }
+
+ static let byteFormatter = ByteCountFormatter()
+
+ private func formattedFileSize(_ fileSize: UInt) -> String {
+ FileItemView.byteFormatter.allowedUnits = [.useAll]
+ FileItemView.byteFormatter.countStyle = .file
+ return FileItemView.byteFormatter.string(fromByteCount: Int64(fileSize))
+ }
+}
diff --git a/Hotline/macOS/FilePreviewImageView.swift b/Hotline/macOS/Files/FilePreviewImageView.swift
index 9beeb80..9beeb80 100644
--- a/Hotline/macOS/FilePreviewImageView.swift
+++ b/Hotline/macOS/Files/FilePreviewImageView.swift
diff --git a/Hotline/macOS/FilePreviewTextView.swift b/Hotline/macOS/Files/FilePreviewTextView.swift
index c286381..c286381 100644
--- a/Hotline/macOS/FilePreviewTextView.swift
+++ b/Hotline/macOS/Files/FilePreviewTextView.swift
diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/Files/FilesView.swift
index 42232af..de699ff 100644
--- a/Hotline/macOS/FilesView.swift
+++ b/Hotline/macOS/Files/FilesView.swift
@@ -2,210 +2,9 @@ import SwiftUI
import UniformTypeIdentifiers
import AppKit
-struct FolderView: View {
- @Environment(Hotline.self) private var model: Hotline
-
- @State var loading = false
- @State var dragOver = false
-
- var file: FileInfo
- let depth: Int
-
- @MainActor private func uploadFile(file fileURL: URL) {
- var filePath: [String] = [String](self.file.path)
- if !self.file.isFolder {
- filePath.removeLast()
- }
-
- print("UPLOADING TO PATH: ", filePath)
-
- model.uploadFile(url: fileURL, path: filePath) { info in
- Task {
- // Refresh file listing to display newly uploaded file.
- let _ = await model.getFileList(path: filePath)
- }
- }
- }
-
- var body: some View {
- HStack(alignment: .center, spacing: 0) {
- Spacer()
- .frame(width: CGFloat(depth * (12 + 2)))
-
- Button {
- if file.isFolder {
- file.expanded.toggle()
- }
- } label: {
- Text(Image(systemName: file.expanded ? "chevron.down" : "chevron.right"))
- .bold()
- .font(.system(size: 10))
- .foregroundStyle(dragOver ? Color.white : Color.primary)
- .opacity(0.5)
- }
- .buttonStyle(.plain)
- .frame(width: 10)
- .padding(.leading, 4)
- .padding(.trailing, 8)
-
- HStack(alignment: .center) {
- if file.isUnavailable {
- Image(systemName: "questionmark.app.fill")
- .frame(width: 16, height: 16)
- .opacity(0.5)
- }
- else if file.isAdminDropboxFolder {
- Image("Admin Drop Box")
- .resizable()
- .scaledToFit()
- .frame(width: 16, height: 16)
- }
- else if file.isDropboxFolder {
- Image("Drop Box")
- .resizable()
- .scaledToFit()
- .frame(width: 16, height: 16)
- }
- else {
- Image("Folder")
- .resizable()
- .scaledToFit()
- .frame(width: 16, height: 16)
- }
- }
- .frame(width: 16)
- .padding(.trailing, 6)
-
- Text(file.name)
- .lineLimit(1)
- .truncationMode(.tail)
- .foregroundStyle(dragOver ? Color.white : Color.primary)
- .opacity(file.isUnavailable ? 0.5 : 1.0)
-
- if loading {
- ProgressView().controlSize(.small).padding([.leading, .trailing], 5)
- }
- Spacer()
- if !file.isUnavailable {
- Text(file.fileSize == 0 ? "Empty" : "^[\(file.fileSize) \("file")](inflect: true)")
- .foregroundStyle(dragOver ? Color.white.opacity(0.75) : Color.secondary)
- .lineLimit(1)
- .padding(.trailing, 6)
- }
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(
- RoundedRectangle(cornerRadius: 4.0)
- .fill(dragOver ? Color(nsColor: NSColor.selectedContentBackgroundColor) : Color.clear)
- .padding(.horizontal, -6)
- .padding(.vertical, -4)
- )
- .onChange(of: file.expanded) {
- loading = false
- if file.expanded && file.fileSize > 0 {
- Task {
- loading = true
- let _ = await model.getFileList(path: file.path)
- loading = false
- }
- }
- }
- .onDrop(of: [.fileURL], isTargeted: $dragOver) { items in
- guard let item = items.first,
- let identifier = item.registeredTypeIdentifiers.first else {
- return false
- }
-
- item.loadItem(forTypeIdentifier: identifier, options: nil) { (urlData, error) in
- DispatchQueue.main.async {
- if let urlData = urlData as? Data,
- let fileURL = URL(dataRepresentation: urlData, relativeTo: nil, isAbsolute: true) {
- uploadFile(file: fileURL)
- }
- }
- }
-
- return true
- }
-
- if file.expanded {
- ForEach(file.children!, id: \.self) { childFile in
- if childFile.isFolder {
- FolderView(file: childFile, depth: self.depth + 1).tag(file.id)
- }
- else {
- FileView(file: childFile, depth: self.depth + 1).tag(file.id)
- }
- }
- }
- }
-}
-struct FileView: View {
- @Environment(Hotline.self) private var model: Hotline
-
- var file: FileInfo
- let depth: Int
-
- var body: some View {
- HStack(alignment: .center, spacing: 0) {
- Spacer()
- .frame(width: CGFloat(depth * (12 + 2)))
-
- Spacer()
- .frame(width: 10)
- .padding(.leading, 4)
- .padding(.trailing, 8)
-
- HStack(alignment: .center) {
- if file.isUnavailable {
- Image(systemName: "questionmark.app.fill")
- .frame(width: 16, height: 16)
- .opacity(0.5)
- }
- else {
- FileIconView(filename: file.name, fileType: file.type)
- .frame(width: 16, height: 16)
- }
- }
- .frame(width: 16)
- .padding(.trailing, 6)
-
- Text(file.name)
- .lineLimit(1)
- .truncationMode(.tail)
- .opacity(file.isUnavailable ? 0.5 : 1.0)
- Spacer()
- if !file.isUnavailable {
- Text(formattedFileSize(file.fileSize))
- .foregroundStyle(.secondary)
- .lineLimit(1)
- .padding(.trailing, 6)
- }
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- if file.expanded {
- ForEach(file.children!, id: \.self) { childFile in
- if childFile.isFolder {
- FolderView(file: childFile, depth: self.depth + 1).tag(file.id)
- }
- else {
- FileView(file: childFile, depth: self.depth + 1).tag(file.id)
- }
- }
- }
- }
-
- static let byteFormatter = ByteCountFormatter()
-
- private func formattedFileSize(_ fileSize: UInt) -> String {
- FileView.byteFormatter.allowedUnits = [.useAll]
- FileView.byteFormatter.countStyle = .file
- return FileView.byteFormatter.string(fromByteCount: Int64(fileSize))
- }
-}
struct FilesView: View {
@Environment(Hotline.self) private var model: Hotline
@@ -332,10 +131,10 @@ struct FilesView: View {
NavigationStack {
List(displayedFiles, id: \.self, selection: $selection) { file in
if file.isFolder {
- FolderView(file: file, depth: 0).tag(file.id)
+ FolderItemView(file: file, depth: 0).tag(file.id)
}
else {
- FileView(file: file, depth: 0).tag(file.id)
+ FileItemView(file: file, depth: 0).tag(file.id)
}
}
.environment(\.defaultMinListRowHeight, 28)
diff --git a/Hotline/macOS/Files/FolderItemView.swift b/Hotline/macOS/Files/FolderItemView.swift
new file mode 100644
index 0000000..4a08974
--- /dev/null
+++ b/Hotline/macOS/Files/FolderItemView.swift
@@ -0,0 +1,140 @@
+import SwiftUI
+
+struct FolderItemView: View {
+ @Environment(Hotline.self) private var model: Hotline
+
+ @State var loading = false
+ @State var dragOver = false
+
+ var file: FileInfo
+ let depth: Int
+
+ @MainActor private func uploadFile(file fileURL: URL) {
+ var filePath: [String] = [String](self.file.path)
+ if !self.file.isFolder {
+ filePath.removeLast()
+ }
+
+ print("UPLOADING TO PATH: ", filePath)
+
+ model.uploadFile(url: fileURL, path: filePath) { info in
+ Task {
+ // Refresh file listing to display newly uploaded file.
+ let _ = await model.getFileList(path: filePath)
+ }
+ }
+ }
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 0) {
+ Spacer()
+ .frame(width: CGFloat(depth * (12 + 2)))
+
+ Button {
+ if file.isFolder {
+ file.expanded.toggle()
+ }
+ } label: {
+ Text(Image(systemName: file.expanded ? "chevron.down" : "chevron.right"))
+ .bold()
+ .font(.system(size: 10))
+ .foregroundStyle(dragOver ? Color.white : Color.primary)
+ .opacity(0.5)
+ }
+ .buttonStyle(.plain)
+ .frame(width: 10)
+ .padding(.leading, 4)
+ .padding(.trailing, 8)
+
+ HStack(alignment: .center) {
+ if file.isUnavailable {
+ Image(systemName: "questionmark.app.fill")
+ .frame(width: 16, height: 16)
+ .opacity(0.5)
+ }
+ else if file.isAdminDropboxFolder {
+ Image("Admin Drop Box")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16)
+ }
+ else if file.isDropboxFolder {
+ Image("Drop Box")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16)
+ }
+ else {
+ Image("Folder")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16)
+ }
+ }
+ .frame(width: 16)
+ .padding(.trailing, 6)
+
+ Text(file.name)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .foregroundStyle(dragOver ? Color.white : Color.primary)
+ .opacity(file.isUnavailable ? 0.5 : 1.0)
+
+ if loading {
+ ProgressView().controlSize(.small).padding([.leading, .trailing], 5)
+ }
+ Spacer()
+ if !file.isUnavailable {
+ Text(file.fileSize == 0 ? "Empty" : "^[\(file.fileSize) \("file")](inflect: true)")
+ .foregroundStyle(dragOver ? Color.white.opacity(0.75) : Color.secondary)
+ .lineLimit(1)
+ .padding(.trailing, 6)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(
+ RoundedRectangle(cornerRadius: 4.0)
+ .fill(dragOver ? Color(nsColor: NSColor.selectedContentBackgroundColor) : Color.clear)
+ .padding(.horizontal, -6)
+ .padding(.vertical, -4)
+ )
+ .onChange(of: file.expanded) {
+ loading = false
+ if file.expanded && file.fileSize > 0 {
+ Task {
+ loading = true
+ let _ = await model.getFileList(path: file.path)
+ loading = false
+ }
+ }
+ }
+ .onDrop(of: [.fileURL], isTargeted: $dragOver) { items in
+ guard let item = items.first,
+ let identifier = item.registeredTypeIdentifiers.first else {
+ return false
+ }
+
+ item.loadItem(forTypeIdentifier: identifier, options: nil) { (urlData, error) in
+ DispatchQueue.main.async {
+ if let urlData = urlData as? Data,
+ let fileURL = URL(dataRepresentation: urlData, relativeTo: nil, isAbsolute: true) {
+ uploadFile(file: fileURL)
+ }
+ }
+ }
+
+ return true
+ }
+
+ if file.expanded {
+ ForEach(file.children!, id: \.self) { childFile in
+ if childFile.isFolder {
+ FolderItemView(file: childFile, depth: self.depth + 1).tag(file.id)
+ }
+ else {
+ FileItemView(file: childFile, depth: self.depth + 1).tag(file.id)
+ }
+ }
+ }
+ }
+}
diff --git a/Hotline/macOS/NewsEditorView.swift b/Hotline/macOS/News/NewsEditorView.swift
index f69c846..f69c846 100644
--- a/Hotline/macOS/NewsEditorView.swift
+++ b/Hotline/macOS/News/NewsEditorView.swift
diff --git a/Hotline/macOS/NewsItemView.swift b/Hotline/macOS/News/NewsItemView.swift
index fc20e61..fc20e61 100644
--- a/Hotline/macOS/NewsItemView.swift
+++ b/Hotline/macOS/News/NewsItemView.swift
diff --git a/Hotline/macOS/NewsView.swift b/Hotline/macOS/News/NewsView.swift
index 91bf2fe..91bf2fe 100644
--- a/Hotline/macOS/NewsView.swift
+++ b/Hotline/macOS/News/NewsView.swift
diff --git a/Hotline/macOS/Settings/GeneralSettingsView.swift b/Hotline/macOS/Settings/GeneralSettingsView.swift
new file mode 100644
index 0000000..6be73aa
--- /dev/null
+++ b/Hotline/macOS/Settings/GeneralSettingsView.swift
@@ -0,0 +1,67 @@
+import SwiftUI
+
+struct GeneralSettingsView: View {
+ @State private var username: String = ""
+ @State private var usernameChanged: Bool = false
+ @State private var showClearHistoryConfirmation: Bool = false
+
+ let saveTimer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
+
+ var body: some View {
+ @Bindable var preferences = Prefs.shared
+
+ Form {
+ TextField("Your Name", text: $username, prompt: Text("guest"))
+ Toggle("Show Join/Leave in Chat", isOn: $preferences.showJoinLeaveMessages)
+ Toggle("Refuse private messages", isOn: $preferences.refusePrivateMessages)
+ Toggle("Refuse private chat", isOn: $preferences.refusePrivateChat)
+ Toggle("Automatic Response", isOn: $preferences.enableAutomaticMessage)
+ if preferences.enableAutomaticMessage {
+ TextField("", text: $preferences.automaticMessage, prompt: Text("Write a response message"))
+ .lineLimit(2)
+ .multilineTextAlignment(.leading)
+ .frame(maxWidth: .infinity)
+ .onSubmit(of: .text) {
+ preferences.username = self.username
+ }
+ }
+
+ Divider()
+
+ Button(role: .destructive) {
+ showClearHistoryConfirmation = true
+ } label: {
+ Text("Clear Chat History…")
+ }
+ }
+ .padding()
+ .frame(width: 392)
+ .confirmationDialog("Clear chat history?", isPresented: $showClearHistoryConfirmation, titleVisibility: .visible) {
+ Button("Clear Chat History", role: .destructive) {
+ Task {
+ await ChatStore.shared.clearAll()
+ }
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("This removes all saved chat logs across servers. Active chats will repopulate only with new messages.")
+ }
+ .onAppear {
+ self.username = preferences.username
+ self.usernameChanged = false
+ }
+ .onDisappear {
+ preferences.username = self.username
+ self.usernameChanged = false
+ }
+ .onChange(of: username) { oldValue, newValue in
+ self.usernameChanged = true
+ }
+ .onReceive(saveTimer) { _ in
+ if self.usernameChanged {
+ self.usernameChanged = false
+ preferences.username = self.username
+ }
+ }
+ }
+}
diff --git a/Hotline/macOS/Settings/IconSettingsView.swift b/Hotline/macOS/Settings/IconSettingsView.swift
new file mode 100644
index 0000000..98cbb09
--- /dev/null
+++ b/Hotline/macOS/Settings/IconSettingsView.swift
@@ -0,0 +1,58 @@
+import SwiftUI
+
+struct IconSettingsView: View {
+ @State private var hoveredUserIconID: Int = -1
+
+ var body: some View {
+ @Bindable var preferences = Prefs.shared
+
+ Form {
+ ScrollViewReader { scrollProxy in
+ ScrollView {
+ LazyVGrid(columns: [
+ GridItem(.fixed(4+32+4)),
+ GridItem(.fixed(4+32+4)),
+ GridItem(.fixed(4+32+4)),
+ GridItem(.fixed(4+32+4)),
+ GridItem(.fixed(4+32+4)),
+ GridItem(.fixed(4+32+4)),
+ GridItem(.fixed(4+32+4))
+ ], spacing: 0) {
+ ForEach(Hotline.classicIconSet, id: \.self) { iconID in
+ HStack {
+ Image("Classic/\(iconID)")
+ .resizable()
+ .interpolation(.none)
+ .scaledToFit()
+ .frame(width: 32, height: 16)
+ .help("Icon \(String(iconID))")
+ }
+ .tag(iconID)
+ .frame(width: 32, height: 32)
+ .padding(4)
+ .background(iconID == preferences.userIconID ? Color.accentColor : (iconID == hoveredUserIconID ? Color.accentColor.opacity(0.1) : Color(nsColor: .textBackgroundColor)))
+ .clipShape(RoundedRectangle(cornerRadius: 5))
+ .onTapGesture {
+ preferences.userIconID = iconID
+ }
+ .onHover { hovered in
+ if hovered {
+ self.hoveredUserIconID = iconID
+ }
+ }
+ }
+ }
+ .padding()
+ }
+ .background(Color(nsColor: .textBackgroundColor))
+ .clipShape(RoundedRectangle(cornerRadius: 6))
+ .overlay(RoundedRectangle(cornerRadius: 6).stroke(Color(nsColor: .separatorColor), lineWidth: 1))
+ .onAppear {
+ scrollProxy.scrollTo(preferences.userIconID, anchor: .center)
+ }
+ .frame(height: 355)
+ }
+ }
+ .padding()
+ }
+}
diff --git a/Hotline/macOS/Settings/SettingsView.swift b/Hotline/macOS/Settings/SettingsView.swift
new file mode 100644
index 0000000..b2a2948
--- /dev/null
+++ b/Hotline/macOS/Settings/SettingsView.swift
@@ -0,0 +1,31 @@
+import SwiftUI
+
+struct SettingsView: View {
+ private enum Tabs: Hashable {
+ case general, icon
+ }
+
+ var body: some View {
+ TabView {
+ GeneralSettingsView()
+ .tabItem {
+ Label("General", systemImage: "person.text.rectangle")
+ }
+ .tag(Tabs.general)
+ IconSettingsView()
+ .tabItem {
+ Label("Icon", systemImage: "person")
+ }
+ .tag(Tabs.icon)
+ SoundSettingsView()
+ .tabItem {
+ Label("Sound", systemImage: "speaker.wave.3")
+ }
+ .tag(Tabs.icon)
+ }
+ }
+}
+
+#Preview {
+ SettingsView()
+}
diff --git a/Hotline/macOS/Settings/SoundSettingsView.swift b/Hotline/macOS/Settings/SoundSettingsView.swift
new file mode 100644
index 0000000..b99db77
--- /dev/null
+++ b/Hotline/macOS/Settings/SoundSettingsView.swift
@@ -0,0 +1,40 @@
+import SwiftUI
+
+struct SoundSettingsView: View {
+ var body: some View {
+ @Bindable var preferences = Prefs.shared
+
+ Form {
+ Toggle("Enable Sounds", isOn: $preferences.playSounds)
+ .controlSize(.large)
+
+ Section("Sounds") {
+ Toggle("Chat", isOn: $preferences.playChatSound)
+ .disabled(!preferences.playSounds)
+
+ Toggle("File Transfers", isOn: $preferences.playFileTransferCompleteSound)
+ .disabled(!preferences.playSounds)
+
+ Toggle("Private Message", isOn: $preferences.playPrivateMessageSound)
+ .disabled(!preferences.playSounds)
+
+ Toggle("Join", isOn: $preferences.playJoinSound)
+ .disabled(!preferences.playSounds)
+
+ Toggle("Leave", isOn: $preferences.playLeaveSound)
+ .disabled(!preferences.playSounds)
+
+ Toggle("Logged in", isOn: $preferences.playLoggedInSound)
+ .disabled(!preferences.playSounds)
+
+ Toggle("Error", isOn: $preferences.playErrorSound)
+ .disabled(!preferences.playSounds)
+
+ Toggle("Chat Invitation", isOn: $preferences.playChatInvitationSound)
+ .disabled(!preferences.playSounds)
+ }
+ }
+ .formStyle(.grouped)
+ .frame(width: 392, height: 433)
+ }
+}
diff --git a/Hotline/macOS/SettingsView.swift b/Hotline/macOS/SettingsView.swift
deleted file mode 100644
index d301abd..0000000
--- a/Hotline/macOS/SettingsView.swift
+++ /dev/null
@@ -1,193 +0,0 @@
-import SwiftUI
-
-struct GeneralSettingsView: View {
- @State private var username: String = ""
- @State private var usernameChanged: Bool = false
- @State private var showClearHistoryConfirmation: Bool = false
-
- let saveTimer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
-
- var body: some View {
- @Bindable var preferences = Prefs.shared
-
- Form {
- TextField("Your Name", text: $username, prompt: Text("guest"))
- Toggle("Show Join/Leave in Chat", isOn: $preferences.showJoinLeaveMessages)
- Toggle("Refuse private messages", isOn: $preferences.refusePrivateMessages)
- Toggle("Refuse private chat", isOn: $preferences.refusePrivateChat)
- Toggle("Automatic Response", isOn: $preferences.enableAutomaticMessage)
- if preferences.enableAutomaticMessage {
- TextField("", text: $preferences.automaticMessage, prompt: Text("Write a response message"))
- .lineLimit(2)
- .multilineTextAlignment(.leading)
- .frame(maxWidth: .infinity)
- .onSubmit(of: .text) {
- preferences.username = self.username
- }
- }
-
- Divider()
-
- Button(role: .destructive) {
- showClearHistoryConfirmation = true
- } label: {
- Text("Clear Chat History…")
- }
- }
- .padding()
- .frame(width: 392)
- .confirmationDialog("Clear chat history?", isPresented: $showClearHistoryConfirmation, titleVisibility: .visible) {
- Button("Clear Chat History", role: .destructive) {
- Task {
- await ChatStore.shared.clearAll()
- }
- }
- Button("Cancel", role: .cancel) {}
- } message: {
- Text("This removes all saved chat logs across servers. Active chats will repopulate only with new messages.")
- }
- .onAppear {
- self.username = preferences.username
- self.usernameChanged = false
- }
- .onDisappear {
- preferences.username = self.username
- self.usernameChanged = false
- }
- .onChange(of: username) { oldValue, newValue in
- self.usernameChanged = true
- }
- .onReceive(saveTimer) { _ in
- if self.usernameChanged {
- self.usernameChanged = false
- preferences.username = self.username
- }
- }
- }
-}
-
-struct IconSettingsView: View {
- @State private var hoveredUserIconID: Int = -1
-
- var body: some View {
- @Bindable var preferences = Prefs.shared
-
- Form {
- ScrollViewReader { scrollProxy in
- ScrollView {
- LazyVGrid(columns: [
- GridItem(.fixed(4+32+4)),
- GridItem(.fixed(4+32+4)),
- GridItem(.fixed(4+32+4)),
- GridItem(.fixed(4+32+4)),
- GridItem(.fixed(4+32+4)),
- GridItem(.fixed(4+32+4)),
- GridItem(.fixed(4+32+4))
- ], spacing: 0) {
- ForEach(Hotline.classicIconSet, id: \.self) { iconID in
- HStack {
- Image("Classic/\(iconID)")
- .resizable()
- .interpolation(.none)
- .scaledToFit()
- .frame(width: 32, height: 16)
- .help("Icon \(String(iconID))")
- }
- .tag(iconID)
- .frame(width: 32, height: 32)
- .padding(4)
- .background(iconID == preferences.userIconID ? Color.accentColor : (iconID == hoveredUserIconID ? Color.accentColor.opacity(0.1) : Color(nsColor: .textBackgroundColor)))
- .clipShape(RoundedRectangle(cornerRadius: 5))
- .onTapGesture {
- preferences.userIconID = iconID
- }
- .onHover { hovered in
- if hovered {
- self.hoveredUserIconID = iconID
- }
- }
- }
- }
- .padding()
- }
- .background(Color(nsColor: .textBackgroundColor))
- .clipShape(RoundedRectangle(cornerRadius: 6))
- .overlay(RoundedRectangle(cornerRadius: 6).stroke(Color(nsColor: .separatorColor), lineWidth: 1))
- .onAppear {
- scrollProxy.scrollTo(preferences.userIconID, anchor: .center)
- }
- .frame(height: 355)
- }
- }
- .padding()
- }
-}
-
-struct SoundSettingsView: View {
- var body: some View {
- @Bindable var preferences = Prefs.shared
-
- Form {
- Toggle("Enable Sounds", isOn: $preferences.playSounds)
- .controlSize(.large)
-
- Section("Sounds") {
- Toggle("Chat", isOn: $preferences.playChatSound)
- .disabled(!preferences.playSounds)
-
- Toggle("File Transfers", isOn: $preferences.playFileTransferCompleteSound)
- .disabled(!preferences.playSounds)
-
- Toggle("Private Message", isOn: $preferences.playPrivateMessageSound)
- .disabled(!preferences.playSounds)
-
- Toggle("Join", isOn: $preferences.playJoinSound)
- .disabled(!preferences.playSounds)
-
- Toggle("Leave", isOn: $preferences.playLeaveSound)
- .disabled(!preferences.playSounds)
-
- Toggle("Logged in", isOn: $preferences.playLoggedInSound)
- .disabled(!preferences.playSounds)
-
- Toggle("Error", isOn: $preferences.playErrorSound)
- .disabled(!preferences.playSounds)
-
- Toggle("Chat Invitation", isOn: $preferences.playChatInvitationSound)
- .disabled(!preferences.playSounds)
- }
- }
- .formStyle(.grouped)
- .frame(width: 392, height: 433)
- }
-}
-
-struct SettingsView: View {
- private enum Tabs: Hashable {
- case general, icon
- }
-
- var body: some View {
- TabView {
- GeneralSettingsView()
- .tabItem {
- Label("General", systemImage: "person.text.rectangle")
- }
- .tag(Tabs.general)
- IconSettingsView()
- .tabItem {
- Label("Icon", systemImage: "person")
- }
- .tag(Tabs.icon)
- SoundSettingsView()
- .tabItem {
- Label("Sound", systemImage: "speaker.wave.3")
- }
- .tag(Tabs.icon)
- }
- }
-}
-
-#Preview {
- SettingsView()
-}
diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift
index 251248f..34f7a57 100644
--- a/Hotline/macOS/TrackerView.swift
+++ b/Hotline/macOS/TrackerView.swift
@@ -105,6 +105,7 @@ struct TrackerView: View {
.moveDisabled(true)
.deleteDisabled(true)
.tag(TrackerSelection.bookmarkServer(trackedServer))
+ .padding(.leading, 16 + 8 + 10)
}
}
}
@@ -180,18 +181,6 @@ struct TrackerView: View {
case .bookmarkServer(let bookmarkServer):
openWindow(id: "server", value: bookmarkServer.server)
}
-
-// if clickedItem.type == .tracker {
-// if NSEvent.modifierFlags.contains(.option) {
-// trackerSheetBookmark = clickedItem
-// }
-// else {
-// clickedItem.expanded.toggle()
-// }
-// }
-// else if let server = clickedItem.server {
-// openWindow(id: "server", value: server)
-// }
}
.fileExporter(isPresented: $bookmarkExportActive, document: bookmarkExport, contentTypes: [.data], defaultFilename: "\(bookmarkExport?.bookmark.name ?? "Hotline Bookmark").hlbm", onCompletion: { result in
switch result {
@@ -208,38 +197,26 @@ struct TrackerView: View {
switch self.selection {
case .bookmark(let bookmark):
if bookmark.type == .tracker {
- self.expandedTrackers.insert(bookmark)
+ self.setExpanded(true, for: bookmark)
return .handled
}
default:
break
}
-// if
-// let bookmark = selection,
-// bookmark.type == .tracker {
-// bookmark.expanded = true
-// return .handled
-// }
return .ignored
}
.onKeyPress(.leftArrow) {
switch self.selection {
case .bookmark(let bookmark):
if bookmark.type == .tracker {
- self.expandedTrackers.remove(bookmark)
+ self.setExpanded(false, for: bookmark)
return .handled
}
default:
break
}
-// if
-// let bookmark = selection,
-// bookmark.type == .tracker {
-// bookmark.expanded = false
-// return .handled
-// }
return .ignored
}
.onDrop(of: [UTType.fileURL], isTargeted: $fileDropActive) { providers, dropPoint in
@@ -354,8 +331,7 @@ struct TrackerView: View {
Button {
NSPasteboard.general.clearContents()
- let displayAddress = server.port == HotlinePorts.DefaultServerPort ?
- server.address : "\(server.address):\(server.port)"
+ let displayAddress = (server.port == HotlinePorts.DefaultServerPort) ? server.address : "\(server.address):\(server.port)"
NSPasteboard.general.setString(displayAddress, forType: .string)
} label: {
Label("Copy Address", systemImage: "doc.on.doc")
@@ -451,22 +427,7 @@ struct TrackerView: View {
func toggleExpanded(for bookmark: Bookmark) {
guard bookmark.type == .tracker else { return }
-
- if self.expandedTrackers.contains(bookmark) {
- // Collapse: cancel ongoing fetch and clear data
- self.fetchTasks[bookmark]?.cancel()
- self.fetchTasks[bookmark] = nil
- self.expandedTrackers.remove(bookmark)
- self.trackerServers[bookmark] = nil
- self.loadingTrackers.remove(bookmark)
- } else {
- // Expand: start fetch task
- self.expandedTrackers.insert(bookmark)
- let task = Task {
- await self.fetchServers(for: bookmark)
- }
- self.fetchTasks[bookmark] = task
- }
+ self.setExpanded(!self.expandedTrackers.contains(bookmark), for: bookmark)
}
func setExpanded(_ expanded: Bool, for bookmark: Bookmark) {
@@ -678,8 +639,6 @@ struct TrackerBookmarkServerView: View {
var body: some View {
HStack(alignment: .center, spacing: 6) {
- Spacer()
- .frame(width: 14 + 8 + 16)
Image("Server")
.resizable()
.scaledToFit()
@@ -720,6 +679,7 @@ struct TrackerItemView: View {
let isLoading: Bool
let count: Int
let onToggleExpanded: () -> Void
+ @Environment(\.appearsActive) private var appearsActive
var body: some View {
HStack(alignment: .center, spacing: 6) {
@@ -759,22 +719,20 @@ struct TrackerItemView: View {
SpinningGlobeView()
.fontWeight(.semibold)
.frame(width: 12, height: 12)
-// .opacity(0.5)
}
-
- .padding(.horizontal, 8)
+ .padding(.horizontal, 6)
.padding(.vertical, 2)
.foregroundStyle(.secondary)
- .background(.quinary)
+// .background(.quinary)
.clipShape(.capsule)
}
case .server:
Image(systemName: "bookmark.fill")
.resizable()
- .renderingMode(.template)
- .aspectRatio(contentMode: .fit)
+ .scaledToFit()
+ .foregroundStyle(Color.secondary)
.frame(width: 11, height: 11, alignment: .center)
- .opacity(0.5)
+ .opacity(0.75)
.padding(.leading, 3)
.padding(.trailing, 2)
Image("Server")