diff options
| author | Dustin Mierau <dustin@mierau.me> | 2025-10-27 21:48:20 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2025-10-27 21:48:20 -0700 |
| commit | 4bb0ffba596e41a8309ba50d222248a0ba3eb42e (patch) | |
| tree | fcd1c2b11c071a624d0b3b56ed16340272200c7d /Hotline | |
| parent | 924012e6ee03fff5570e9362e29be9b04816b777 (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.swift | 67 | ||||
| -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.swift | 140 | ||||
| -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.swift | 67 | ||||
| -rw-r--r-- | Hotline/macOS/Settings/IconSettingsView.swift | 58 | ||||
| -rw-r--r-- | Hotline/macOS/Settings/SettingsView.swift | 31 | ||||
| -rw-r--r-- | Hotline/macOS/Settings/SoundSettingsView.swift | 40 | ||||
| -rw-r--r-- | Hotline/macOS/SettingsView.swift | 193 | ||||
| -rw-r--r-- | Hotline/macOS/TrackerView.swift | 64 |
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") |