diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-02-05 22:27:17 +0100 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-02-05 22:27:17 +0100 |
| commit | 45829daa856b376b1ab04d415917110b71b1dec5 (patch) | |
| tree | 8e52daed6897b6f489d455736fe256cb9bd90fef /Hotline/macOS | |
| parent | 5c3ea897d062a47bc8cd6255fb8c36bad2f0733f (diff) | |
Apply formatting
Diffstat (limited to 'Hotline/macOS')
| -rw-r--r-- | Hotline/macOS/AboutView.swift | 114 | ||||
| -rw-r--r-- | Hotline/macOS/AccountManagerView.swift | 117 | ||||
| -rw-r--r-- | Hotline/macOS/ChatView.swift | 161 | ||||
| -rw-r--r-- | Hotline/macOS/FileDetailsView.swift | 62 | ||||
| -rw-r--r-- | Hotline/macOS/FilePreviewImageView.swift | 24 | ||||
| -rw-r--r-- | Hotline/macOS/FilePreviewTextView.swift | 22 | ||||
| -rw-r--r-- | Hotline/macOS/FilesView.swift | 182 | ||||
| -rw-r--r-- | Hotline/macOS/HotlinePanelView.swift | 110 | ||||
| -rw-r--r-- | Hotline/macOS/MessageBoardEditorView.swift | 60 | ||||
| -rw-r--r-- | Hotline/macOS/MessageBoardView.swift | 63 | ||||
| -rw-r--r-- | Hotline/macOS/MessageView.swift | 34 | ||||
| -rw-r--r-- | Hotline/macOS/NewsEditorView.swift | 52 | ||||
| -rw-r--r-- | Hotline/macOS/NewsItemView.swift | 97 | ||||
| -rw-r--r-- | Hotline/macOS/NewsView.swift | 96 | ||||
| -rw-r--r-- | Hotline/macOS/ServerAgreementView.swift | 60 | ||||
| -rw-r--r-- | Hotline/macOS/ServerMessageView.swift | 12 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 346 | ||||
| -rw-r--r-- | Hotline/macOS/SettingsView.swift | 47 | ||||
| -rw-r--r-- | Hotline/macOS/TrackerView.swift | 181 |
19 files changed, 943 insertions, 897 deletions
diff --git a/Hotline/macOS/AboutView.swift b/Hotline/macOS/AboutView.swift index baafcf8..a2771b9 100644 --- a/Hotline/macOS/AboutView.swift +++ b/Hotline/macOS/AboutView.swift @@ -16,31 +16,31 @@ struct AboutContributor: Identifiable { struct AboutView: View { @Environment(\.openURL) private var openURL - + @State private var versionCheck: VersionCheckState = .needToCheck @State private var downloadURL: String = "https://github.com/mierau/hotline/releases/latest" @State private var contributors: [AboutContributor] = [] - + var body: some View { HStack(alignment: .center, spacing: 0) { VStack(alignment: .center, spacing: 0) { Spacer() - + Image("About Hotline") .padding(.top, 44) - + Text("Hotline") .font(.system(size: 28)) .fontWeight(.bold) .padding(.top, 12) .kerning(-1.0) .foregroundColor(.white) - + let appDetails = getAppVersionAndBuild() Text("Version \(String(format: "%.1f", appDetails.version))b\(appDetails.build)") .foregroundColor(.white) .opacity(0.4) - + HStack(alignment: .center) { switch versionCheck { case .needToCheck: @@ -81,12 +81,12 @@ struct AboutView: View { Spacer() } .frame(width: 250) - + Spacer() - + ScrollView(.vertical) { VStack(alignment: .leading, spacing: 16) { - + VStack(alignment: .leading, spacing: 4) { Link(destination: URL(string: "https://github.com/mierau/hotline")!) { HStack(alignment: .center, spacing: 4) { @@ -107,7 +107,7 @@ struct AboutView: View { } } .padding(.top, 24) - + Text("Hotline is an open source project made possible by its contributors.") .font(.system(size: 11)) .foregroundStyle(.black) @@ -115,7 +115,7 @@ struct AboutView: View { .padding(.trailing, 32) } .padding(.bottom, 8) - + ForEach(contributors) { contributor in Link(destination: contributor.webURL) { HStack { @@ -139,27 +139,27 @@ struct AboutView: View { } .frame(width: 32, height: 32) .clipShape(Circle()) - -// AsyncImage(url: pictureURL) { img in -// img -// .interpolation(.high) -// .resizable() -// .scaledToFit() -// .background(.white) -// } placeholder: { -// Color.white.opacity(0.2) -// .frame(width: 32, height: 32) -// } -// .frame(width: 32, height: 32) -// .clipShape(Circle()) + + // AsyncImage(url: pictureURL) { img in + // img + // .interpolation(.high) + // .resizable() + // .scaledToFit() + // .background(.white) + // } placeholder: { + // Color.white.opacity(0.2) + // .frame(width: 32, height: 32) + // } + // .frame(width: 32, height: 32) + // .clipShape(Circle()) } - + VStack(alignment: .leading, spacing: 2) { Text(contributor.username) .fontWeight(.semibold) .foregroundStyle(.white) .lineLimit(1) - + Text(contributor.webURL.absoluteString) .lineLimit(1) .truncationMode(.middle) @@ -189,68 +189,72 @@ struct AboutView: View { await loadContributors() } } - + func loadContributors() async { var newContributors: [AboutContributor] = [] - + if let url = URL(string: "https://api.github.com/repos/mierau/hotline/contributors"), - let (data, _) = try? await URLSession.shared.data(from: url) { - if let jsonContributors = try? JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] { + let (data, _) = try? await URLSession.shared.data(from: url) + { + if let jsonContributors = try? JSONSerialization.jsonObject(with: data, options: []) + as? [[String: Any]] + { for contributor in jsonContributors { if let username = contributor["login"] as? String, - let webURLString = contributor["html_url"] as? String, - let webURL = URL(string: webURLString) { + let webURLString = contributor["html_url"] as? String, + let webURL = URL(string: webURLString) + { var pictureURL: URL? = nil if let pictureURLString = contributor["avatar_url"] as? String { pictureURL = URL(string: pictureURLString) } - newContributors.append(AboutContributor(username: username, webURL: webURL, pictureURL: pictureURL)) + newContributors.append( + AboutContributor(username: username, webURL: webURL, pictureURL: pictureURL)) } } } } - + withAnimation { contributors = newContributors } } - + func checkForUpdate() async { let appDetails = getAppVersionAndBuild() - + self.versionCheck = .checking - + do { let url = URL(string: "https://api.github.com/repos/mierau/hotline/releases/latest")! let (data, _) = try await URLSession.shared.data(from: url) let versionExpression = /^([0-9\.]+)beta([0-9]+)/ - + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let tagName = json["tag_name"] as? String, - let assets = json["assets"] as? [[String: Any]], - let firstAsset = assets.first, - let assetDownloadURL = firstAsset["browser_download_url"] as? String, - let versionMatches = try? versionExpression.wholeMatch(in: tagName) { + let tagName = json["tag_name"] as? String, + let assets = json["assets"] as? [[String: Any]], + let firstAsset = assets.first, + let assetDownloadURL = firstAsset["browser_download_url"] as? String, + let versionMatches = try? versionExpression.wholeMatch(in: tagName) + { if let versionNumber = Double(versionMatches.1), - let buildNumber = Int(versionMatches.2), - versionNumber > appDetails.version || buildNumber > appDetails.build { - let versionString = "\(versionMatches.1)b\(versionMatches.2)" - self.versionCheck = .updateAvailable(version: versionString) - downloadURL = assetDownloadURL - } - else { + let buildNumber = Int(versionMatches.2), + versionNumber > appDetails.version || buildNumber > appDetails.build + { + let versionString = "\(versionMatches.1)b\(versionMatches.2)" + self.versionCheck = .updateAvailable(version: versionString) + downloadURL = assetDownloadURL + } else { self.versionCheck = .upToDate } - } - else { + } else { self.versionCheck = .needToCheck } - } - catch { + } catch { self.versionCheck = .needToCheck } } - + func getAppVersionAndBuild() -> (version: Double, build: Int) { let infoDictionary = Bundle.main.infoDictionary! let version = Double(infoDictionary["CFBundleShortVersionString"]! as! String)! diff --git a/Hotline/macOS/AccountManagerView.swift b/Hotline/macOS/AccountManagerView.swift index 57682cc..84ae6dc 100644 --- a/Hotline/macOS/AccountManagerView.swift +++ b/Hotline/macOS/AccountManagerView.swift @@ -2,20 +2,20 @@ import SwiftUI struct AccountManagerView: View { @Environment(Hotline.self) private var model: Hotline - + @State private var accounts: [HotlineAccount] = [] @State private var selection: HotlineAccount? @State private var loading: Bool = true - + @State private var pendingName: String = "" @State private var pendingLogin: String = "" @State private var pendingPassword: String = "" @State private var pendingAccess = HotlineUserAccessOptions.defaultAccess - + @State private var toDelete: HotlineAccount? - + let placeholderPassword = "xxxxxxxxxxxxxxxxxx" - + var body: some View { HStack(spacing: 0) { ZStack { @@ -26,8 +26,7 @@ struct AccountManagerView: View { } if selection != nil { accountDetails - } - else { + } else { ZStack(alignment: .center) { Text("No Account Selected") .font(.title) @@ -50,8 +49,9 @@ struct AccountManagerView: View { .toolbar { ToolbarItem(placement: .primaryAction) { Button { - let newAccount = HotlineAccount("unnamed", "unnamed", HotlineUserAccessOptions.defaultAccess) - + let newAccount = HotlineAccount( + "unnamed", "unnamed", HotlineUserAccessOptions.defaultAccess) + pendingPassword = HotlineAccount.randomPassword() accounts.append(newAccount) selection = newAccount @@ -61,7 +61,7 @@ struct AccountManagerView: View { .help("Create a new account") .disabled(model.access?.contains(.canCreateUsers) != true) } - + ToolbarItem(placement: .destructiveAction) { Button { toDelete = selection @@ -73,7 +73,7 @@ struct AccountManagerView: View { } } } - + var accountDetails: some View { VStack(alignment: .center, spacing: 0) { ScrollView(.vertical) { @@ -92,7 +92,7 @@ struct AccountManagerView: View { } .textFieldStyle(.roundedBorder) .controlSize(.large) - + Section("File System Maintenance") { Toggle("Can Download Files", isOn: $pendingAccess.bind(.canDownloadFiles)) .disabled(model.access?.contains(.canDownloadFiles) == false) @@ -127,7 +127,7 @@ struct AccountManagerView: View { Toggle("Can Make Aliases", isOn: $pendingAccess.bind(.canMakeAliases)) .disabled(model.access?.contains(.canMakeAliases) == false) } - + Section("User Maintenance") { Toggle("Can Create Accounts", isOn: $pendingAccess.bind(.canCreateUsers)) .disabled(model.access?.contains(.canCreateUsers) == false) @@ -139,20 +139,20 @@ struct AccountManagerView: View { .disabled(model.access?.contains(.canModifyUsers) == false) Toggle("Can Get User Info", isOn: $pendingAccess.bind(.canGetClientInfo)) .disabled(model.access?.contains(.canGetClientInfo) == false) - + Toggle("Can Disconnect Users", isOn: $pendingAccess.bind(.canDisconnectUsers)) .disabled(model.access?.contains(.canDisconnectUsers) == false) Toggle("Cannot be Disconnected", isOn: $pendingAccess.bind(.cantBeDisconnected)) .disabled(model.access?.contains(.cantBeDisconnected) == false) } - + Section("Messaging") { Toggle("Can Send Messages", isOn: $pendingAccess.bind(.canSendMessages)) .disabled(model.access?.contains(.canSendMessages) == false) Toggle("Can Broadcast", isOn: $pendingAccess.bind(.canBroadcast)) .disabled(model.access?.contains(.canBroadcast) == false) } - + Section("News") { Toggle("Can Read Articles", isOn: $pendingAccess.bind(.canReadMessageBoard)) .disabled(model.access?.contains(.canReadMessageBoard) == false) @@ -169,7 +169,7 @@ struct AccountManagerView: View { Toggle("Can Delete News Bundles", isOn: $pendingAccess.bind(.canDeleteNewsFolders)) .disabled(model.access?.contains(.canDeleteNewsFolders) == false) } - + Section("Chat") { Toggle("Can Initiate Private Chat", isOn: $pendingAccess.bind(.canCreateChat)) .disabled(model.access?.contains(.canCreateChat) == false) @@ -178,7 +178,7 @@ struct AccountManagerView: View { Toggle("Can Send Chat", isOn: $pendingAccess.bind(.canSendChat)) .disabled(model.access?.contains(.canSendChat) == false) } - + Section("Miscellaneous") { Toggle("Can Use Any Name", isOn: $pendingAccess.bind(.canUseAnyName)) .disabled(model.access?.contains(.canUseAnyName) == false) @@ -193,7 +193,7 @@ struct AccountManagerView: View { pendingName = selection.name pendingLogin = selection.login pendingAccess = selection.access - + if selection.persisted { if selection.password == nil { pendingPassword = "" @@ -203,12 +203,12 @@ struct AccountManagerView: View { } } } - .onAppear() { + .onAppear { if let selection { pendingName = selection.name pendingLogin = selection.login pendingAccess = selection.access - + if selection.persisted { if selection.password == nil { pendingPassword = "" @@ -216,22 +216,22 @@ struct AccountManagerView: View { pendingPassword = placeholderPassword } } else { - pendingPassword = HotlineAccount.randomPassword() + pendingPassword = HotlineAccount.randomPassword() } } } } .frame(maxWidth: .infinity) - + Divider() - - HStack() { + + HStack { Button("Revert") { if let selection { pendingAccess = selection.access pendingName = selection.name pendingLogin = selection.login - + if selection.password != nil { pendingPassword = selection.password! } @@ -240,46 +240,52 @@ struct AccountManagerView: View { .controlSize(.large) .frame(minWidth: 75) .disabled(!self.isSaveable()) -// .padding() - + // .padding() + Spacer() - - Button("Save"){ + + Button("Save") { guard let selection else { return } - + // Update existing account if selection.persisted == true { if pendingPassword == placeholderPassword { Task { @MainActor in - model.client.sendSetUser(name: pendingName, login: pendingLogin, newLogin: nil, password: nil, access: pendingAccess.rawValue) + model.client.sendSetUser( + name: pendingName, login: pendingLogin, newLogin: nil, password: nil, + access: pendingAccess.rawValue) } } else { Task { @MainActor in - model.client.sendSetUser(name: pendingName, login: pendingLogin, newLogin: nil, password: pendingPassword, access: pendingAccess.rawValue) + model.client.sendSetUser( + name: pendingName, login: pendingLogin, newLogin: nil, password: pendingPassword, + access: pendingAccess.rawValue) } } } else { // Create new existing account Task { @MainActor in - model.client.sendCreateUser(name: pendingName, login: pendingLogin, password: pendingPassword, access: pendingAccess.rawValue) + model.client.sendCreateUser( + name: pendingName, login: pendingLogin, password: pendingPassword, + access: pendingAccess.rawValue) } self.selection?.password = pendingPassword pendingPassword = placeholderPassword } - + var account = HotlineAccount(pendingName, pendingLogin, pendingAccess) account.persisted = true account.password = placeholderPassword - + accounts = accounts.filter { $0.persisted == true && $0.login != selection.login } - + // Add new account to list accounts.append(account) - + // Re-sort accounts accounts.sort { $0.login < $1.login } self.selection = account @@ -291,9 +297,9 @@ struct AccountManagerView: View { } .padding() } - + } - + var accountList: some View { List(accounts, id: \.self, selection: $selection) { account in HStack(spacing: 5) { @@ -304,8 +310,7 @@ struct AccountManagerView: View { // .padding(.leading, 4) Text(account.login) .foregroundStyle(Color.hotlineRed) - } - else if account.access.rawValue == 0 { + } else if account.access.rawValue == 0 { Image("User") .frame(width: 16, height: 16) // .padding(.leading, 4) @@ -333,65 +338,67 @@ struct AccountManagerView: View { .frame(width: 250) .sheet(item: $toDelete) { item in Form { - HStack{ + HStack { Image(systemName: "exclamationmark.triangle") .font(.system(size: 30)) Text("Delete account \"\(item.name)\" and all associated files?") .lineSpacing(4) } } - .frame(minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 100, idealHeight: 100, maxHeight: .infinity) + .frame( + minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 100, idealHeight: 100, + maxHeight: .infinity + ) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { toDelete = nil } } - + ToolbarItem(placement: .primaryAction) { Button("Delete") { guard let userToDelete = toDelete else { return } - + self.toDelete = nil self.selection = nil - + if userToDelete.persisted { Task { @MainActor in model.client.sendDeleteUser(login: userToDelete.login) } } - + accounts = accounts.filter { $0.login != userToDelete.login } - + } } } } } - - + private func isSaveable() -> Bool { guard let selection else { return false } - + // Disable save if login field is cleared if pendingLogin == "" { return false } - + // If the account initial has a password and it was updated if selection.password != nil && pendingPassword != placeholderPassword { return true } - + // If the account initial has no password, but was updated to have one if selection.password == nil && pendingPassword != "" { return true } - + // If the access bits or user name have been changed return pendingAccess.rawValue != selection.access.rawValue || selection.name != pendingName } diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift index cd84369..33a43b1 100644 --- a/Hotline/macOS/ChatView.swift +++ b/Hotline/macOS/ChatView.swift @@ -8,34 +8,34 @@ struct ChatView: View { @Environment(Hotline.self) private var model: Hotline @Environment(\.colorScheme) var colorScheme @Environment(\.dismiss) var dismiss - + @State var input: String = "" @State private var scrollPos: Int? @State private var contentHeight: CGFloat = 0 - + @FocusState private var focusedField: FocusedField? - + @Namespace var bottomID - + @State private var showingExporter: Bool = false - + @State private var chatDocument: TextFile = TextFile() - + var body: some View { NavigationStack { ScrollViewReader { reader in VStack(alignment: .leading, spacing: 0) { - + // MARK: Scroll View GeometryReader { gm in ScrollView(.vertical) { LazyVStack(alignment: .leading) { - + ForEach(model.chat) { msg in - + // MARK: Agreement if msg.type == .agreement { -#if os(iOS) + #if os(iOS) if let bannerImage = self.model.bannerImage { Image(uiImage: bannerImage) .resizable() @@ -43,9 +43,9 @@ struct ChatView: View { .frame(maxWidth: 468.0) .clipShape(RoundedRectangle(cornerRadius: 3)) } -#endif + #endif ServerAgreementView(text: msg.text) - .padding(.bottom, 16) + .padding(.bottom, 16) } // MARK: Server Message else if msg.type == .server { @@ -64,50 +64,51 @@ struct ChatView: View { Spacer() } .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) - } - else { + } else { HStack(alignment: .firstTextBaseline) { if let username = msg.username { -// if msg.text.isImageURL() { -// HStack(alignment: .bottom) { -// Text("**\(username):** ") -// -// let imageURL = URL(string: msg.text)! -// AsyncImage(url: imageURL) { phase in -// switch phase { -// case .failure: -// Text(LocalizedStringKey(msg.text.convertLinksToMarkdown())) -// .lineSpacing(4) -// .multilineTextAlignment(.leading) -// .textSelection(.enabled) -// .tint(Color("Link Color")) -// case .success(let img): -// Link(destination: imageURL) { -// img -// .resizable() -// .scaledToFit() -// .frame(maxWidth: 250, maxHeight: 150, alignment: .leading) -// .onAppear { -// reader.scrollTo(bottomID, anchor: .bottom) -// } -// } -// default: -// ProgressView().controlSize(.small) -// } -// } -// -// Spacer() -// } -// } -// else { - Text(LocalizedStringKey("**\(username):** \(msg.text)".convertingLinksToMarkdown())) - .lineSpacing(4) - .multilineTextAlignment(.leading) - .textSelection(.enabled) - .tint(Color("Link Color")) -// } - } - else { + // if msg.text.isImageURL() { + // HStack(alignment: .bottom) { + // Text("**\(username):** ") + // + // let imageURL = URL(string: msg.text)! + // AsyncImage(url: imageURL) { phase in + // switch phase { + // case .failure: + // Text(LocalizedStringKey(msg.text.convertLinksToMarkdown())) + // .lineSpacing(4) + // .multilineTextAlignment(.leading) + // .textSelection(.enabled) + // .tint(Color("Link Color")) + // case .success(let img): + // Link(destination: imageURL) { + // img + // .resizable() + // .scaledToFit() + // .frame(maxWidth: 250, maxHeight: 150, alignment: .leading) + // .onAppear { + // reader.scrollTo(bottomID, anchor: .bottom) + // } + // } + // default: + // ProgressView().controlSize(.small) + // } + // } + // + // Spacer() + // } + // } + // else { + Text( + LocalizedStringKey( + "**\(username):** \(msg.text)".convertingLinksToMarkdown()) + ) + .lineSpacing(4) + .multilineTextAlignment(.leading) + .textSelection(.enabled) + .tint(Color("Link Color")) + // } + } else { Text(msg.text) .lineSpacing(4) .multilineTextAlignment(.leading) @@ -137,10 +138,10 @@ struct ChatView: View { reader.scrollTo(bottomID, anchor: .bottom) } } - + // MARK: Input Divider Divider() - + // MARK: Input Bar HStack(alignment: .lastTextBaseline, spacing: 0) { TextField("", text: $input, axis: .vertical) @@ -178,59 +179,59 @@ struct ChatView: View { } } .background(Color(nsColor: .textBackgroundColor)) -// .toolbar { -// ToolbarItem(placement: .primaryAction) { -// Button { -// if prepareChatDocument() { -// showingExporter = true -// } -// } label: { -// Image(systemName: "square.and.arrow.up") -// }.help("Save Chat...") -// } -// } - .fileExporter(isPresented: $showingExporter, document: self.chatDocument, contentType: .utf8PlainText, defaultFilename: "\(self.model.serverTitle) Chat.txt") { result in + // .toolbar { + // ToolbarItem(placement: .primaryAction) { + // Button { + // if prepareChatDocument() { + // showingExporter = true + // } + // } label: { + // Image(systemName: "square.and.arrow.up") + // }.help("Save Chat...") + // } + // } + .fileExporter( + isPresented: $showingExporter, document: self.chatDocument, contentType: .utf8PlainText, + defaultFilename: "\(self.model.serverTitle) Chat.txt" + ) { result in switch result { case .success(let url): print("Saved to \(url)") - + case .failure(let error): print(error.localizedDescription) } self.chatDocument.text = "" } } - + private func prepareChatDocument() -> Bool { var text: String = String() - + self.chatDocument.text = "" for msg in model.chat { if msg.type == .agreement { text.append(msg.text) text.append("\n\n") - } - else if msg.type == .message { + } else if msg.type == .message { if let username = msg.username { text.append("\(username): \(msg.text)") - } - else { + } else { text.append(msg.text) } text.append("\n") - } - else if msg.type == .status { + } else if msg.type == .status { text.append(msg.text) text.append("\n") } } - + if text.isEmpty { return false } - + self.chatDocument.text = text - + return true } } diff --git a/Hotline/macOS/FileDetailsView.swift b/Hotline/macOS/FileDetailsView.swift index 34e083b..16b7a60 100644 --- a/Hotline/macOS/FileDetailsView.swift +++ b/Hotline/macOS/FileDetailsView.swift @@ -6,45 +6,45 @@ struct FileDetailsView: View { @Environment(\.presentationMode) var presentationMode var fd: FileDetails - + @State private var comment: String = "" @State private var filename: String = "" - + var body: some View { - VStack (alignment: .leading){ + VStack(alignment: .leading) { Form { - HStack(alignment: .center){ + HStack(alignment: .center) { FileIconView(filename: fd.name, fileType: nil) .frame(width: 32, height: 32) TextField("", text: $filename) .disabled(!self.canRename()) } - HStack(alignment: .center){ + HStack(alignment: .center) { Text("Type:").bold().padding(.leading, 43) Text(fd.type) } - HStack(alignment: .center){ + HStack(alignment: .center) { Text("Creator:").bold().padding(.leading, 26) Text(fd.creator) } - HStack(alignment: .center){ + HStack(alignment: .center) { Text("Size:").bold().bold().padding(.leading, 48) Text(self.formattedSize(byteCount: fd.size)) } - HStack(alignment: .center){ + HStack(alignment: .center) { Text("Created:").bold().padding(.leading, 24) Text("\(FileDetailsView.dateFormatter.string(from: fd.created))") } - HStack(alignment: .center){ + HStack(alignment: .center) { Text("Modified:").bold().padding(.leading, 19) Text("\(FileDetailsView.dateFormatter.string(from: fd.modified))") } - HStack(alignment: .center){ + HStack(alignment: .center) { Text("Comments:").bold().padding(.top, 8) .padding(.leading, 5) } - - VStack(alignment: .trailing){ + + VStack(alignment: .trailing) { TextEditor(text: $comment) .padding(.leading, 2) .padding(.top, 1) @@ -61,22 +61,23 @@ struct FileDetailsView: View { presentationMode.wrappedValue.dismiss() } } - + ToolbarItem(placement: .primaryAction) { - Button{ + Button { var editedFilename: String? if filename != fd.name { editedFilename = filename } - + var editedComment: String? if comment != fd.comment { editedComment = comment } - - model.client.sendSetFileInfo(fileName: fd.name, path: fd.path, fileNewName: editedFilename, comment: editedComment) + + model.client.sendSetFileInfo( + fileName: fd.name, path: fd.path, fileNewName: editedFilename, comment: editedComment) presentationMode.wrappedValue.dismiss() - + // TODO: Update the file list if the filename was changed } label: { Text("Save") @@ -92,48 +93,49 @@ struct FileDetailsView: View { } .frame(minWidth: 400, minHeight: 400) } - - + static var dateFormatter: DateFormatter = { var dateFormatter = DateFormatter() dateFormatter.dateStyle = .long dateFormatter.timeStyle = .short - + // Original format: Fri, Aug 20, 2021, 5:14:07 PM return dateFormatter }() - + static var byteCountSizeFormatter: NumberFormatter = { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal return numberFormatter }() - + 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)) } - + // 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)" + let formattedByteCount = + FileDetailsView.byteCountSizeFormatter.string(from: NSNumber(value: byteCount)) ?? "0" + return + "\(FileView.byteFormatter.string(fromByteCount: Int64(byteCount))) (\(formattedByteCount) bytes)" } - + private func isEdited() -> Bool { return self.filename != fd.name || self.comment != fd.comment } - + private func canRename() -> Bool { if self.fd.type == "fldr" { return model.access?.contains(.canRenameFolders) == true } return model.access?.contains(.canRenameFiles) == true } - + private func canSetComment() -> Bool { if self.fd.type == "fldr" { return model.access?.contains(.canSetFolderComment) == true diff --git a/Hotline/macOS/FilePreviewImageView.swift b/Hotline/macOS/FilePreviewImageView.swift index 9beeb80..3aa37d3 100644 --- a/Hotline/macOS/FilePreviewImageView.swift +++ b/Hotline/macOS/FilePreviewImageView.swift @@ -5,16 +5,16 @@ struct FilePreviewImageView: View { enum FilePreviewFocus: Hashable { case window } - + @Environment(\.controlActiveState) private var controlActiveState @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) var dismiss - + @Binding var info: PreviewFileInfo? - + @State var preview: FilePreview? = nil @FocusState private var focusField: FilePreviewFocus? - + var body: some View { Group { if preview?.state != .loaded { @@ -28,16 +28,14 @@ struct FilePreviewImageView: View { } .frame(minWidth: 350, maxWidth: 350, minHeight: 150, maxHeight: 150) .padding() - } - else { + } else { if let image = preview?.image { FileImageView(image: image) .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) - } - else { + } else { VStack(alignment: .center, spacing: 0) { Spacer() - + Image(systemName: "eye.trianglebadge.exclamationmark") .resizable() .scaledToFit() @@ -53,7 +51,7 @@ struct FilePreviewImageView: View { .font(.system(size: 14.0)) .frame(maxWidth: 300) .multilineTextAlignment(.center) - + Spacer() } .frame(minWidth: 350, maxWidth: 350, minHeight: 150, maxHeight: 150) @@ -73,7 +71,7 @@ struct FilePreviewImageView: View { .frame(width: 16, height: 16) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) } - + if let img = preview?.image { if let info = info { ToolbarItem(placement: .primaryAction) { @@ -84,7 +82,7 @@ struct FilePreviewImageView: View { } .help("Download Image") } - + ToolbarItem(placement: .primaryAction) { ShareLink(item: img, preview: SharePreview(info.name, image: img)) { Label("Share Image...", systemImage: "square.and.arrow.up") @@ -107,7 +105,7 @@ struct FilePreviewImageView: View { } return } - + focusField = .window } .onDisappear { diff --git a/Hotline/macOS/FilePreviewTextView.swift b/Hotline/macOS/FilePreviewTextView.swift index c286381..6dc1529 100644 --- a/Hotline/macOS/FilePreviewTextView.swift +++ b/Hotline/macOS/FilePreviewTextView.swift @@ -5,15 +5,15 @@ struct FilePreviewTextView: View { enum FilePreviewFocus: Hashable { case window } - + @Environment(\.controlActiveState) private var controlActiveState @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) var dismiss - + @Binding var info: PreviewFileInfo? @State var preview: FilePreview? = nil @FocusState private var focusField: FilePreviewFocus? - + var body: some View { Group { if preview?.state != .loaded { @@ -31,8 +31,7 @@ struct FilePreviewTextView: View { .background(Color(nsColor: .textBackgroundColor)) .frame(minWidth: 350, maxWidth: .infinity, minHeight: 150, maxHeight: .infinity) .padding() - } - else { + } else { if let text = preview?.text { TextEditor(text: .constant(text)) .textEditorStyle(.plain) @@ -44,11 +43,10 @@ struct FilePreviewTextView: View { .contentMargins(.trailing, -16.0, for: .scrollIndicators) .scrollClipDisabled() .frame(maxWidth: .infinity, maxHeight: .infinity) - } - else { + } else { VStack(alignment: .center, spacing: 0) { Spacer() - + Image(systemName: "eye.trianglebadge.exclamationmark") .resizable() .scaledToFit() @@ -64,7 +62,7 @@ struct FilePreviewTextView: View { .font(.system(size: 14.0)) .frame(maxWidth: 300) .multilineTextAlignment(.center) - + Spacer() Spacer() } @@ -84,8 +82,8 @@ struct FilePreviewTextView: View { .frame(width: 16, height: 16) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) } - - if let _ = preview?.text { + + if preview?.text != nil { if let info = info { ToolbarItem(placement: .primaryAction) { Button { @@ -111,7 +109,7 @@ struct FilePreviewTextView: View { } return } - + focusField = .window } .onDisappear { diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift index 57a9aeb..fd5826f 100644 --- a/Hotline/macOS/FilesView.swift +++ b/Hotline/macOS/FilesView.swift @@ -3,21 +3,21 @@ import UniformTypeIdentifiers 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. @@ -25,12 +25,12 @@ struct FolderView: View { } } } - + var body: some View { HStack(alignment: .center, spacing: 0) { Spacer() .frame(width: CGFloat(depth * (12 + 2))) - + Button { if file.isFolder { file.expanded.toggle() @@ -46,26 +46,23 @@ struct FolderView: View { .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 { + } else if file.isAdminDropboxFolder { Image("Admin Drop Box") .resizable() .scaledToFit() .frame(width: 16, height: 16) - } - else if file.isDropboxFolder { + } else if file.isDropboxFolder { Image("Drop Box") .resizable() .scaledToFit() .frame(width: 16, height: 16) - } - else { + } else { Image("Folder") .resizable() .scaledToFit() @@ -74,13 +71,13 @@ struct FolderView: View { } .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) } @@ -111,28 +108,29 @@ struct FolderView: View { } .onDrop(of: [.fileURL], isTargeted: $dragOver) { items in guard let item = items.first, - let identifier = item.registeredTypeIdentifiers.first else { + 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) { + 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 { + } else { FileView(file: childFile, depth: self.depth + 1).tag(file.id) } } @@ -142,34 +140,33 @@ struct FolderView: View { 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 { + } 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) @@ -189,16 +186,15 @@ struct FileView: View { ForEach(file.children!, id: \.self) { childFile in if childFile.isFolder { FolderView(file: childFile, depth: self.depth + 1).tag(file.id) - } - else { + } 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 @@ -209,11 +205,11 @@ struct FileView: View { struct FilesView: View { @Environment(Hotline.self) private var model: Hotline @Environment(\.openWindow) private var openWindow - + @State private var selection: FileInfo? @State private var fileDetails: FileDetails? @State private var uploadFileSelectorDisplayed: Bool = false - + private func openPreviewWindow(_ previewInfo: PreviewFileInfo) { switch previewInfo.previewType { case .image: @@ -224,7 +220,7 @@ struct FilesView: View { return } } - + @MainActor private func getFileInfo(_ file: FileInfo) { Task { if let fileInfo = await model.getFileDetails(file.name, path: file.path) { @@ -234,15 +230,15 @@ struct FilesView: View { } } } - + @MainActor private func downloadFile(_ file: FileInfo) { guard !file.isFolder else { return } - + model.downloadFile(file.name, path: file.path) } - + @MainActor private func uploadFile(file fileURL: URL, to path: [String]) { model.uploadFile(url: fileURL, path: path) { info in Task { @@ -251,30 +247,30 @@ struct FilesView: View { } } } - + @MainActor private func previewFile(_ file: FileInfo) { guard file.isPreviewable else { return } - + model.previewFile(file.name, path: file.path) { info in if let info = info { openPreviewWindow(info) } } } - + private func deleteFile(_ file: FileInfo) async { var parentPath: [String] = [] if file.path.count > 1 { - parentPath = Array(file.path[0..<file.path.count-1]) + parentPath = Array(file.path[0..<file.path.count - 1]) } - + if await model.deleteFile(file.name, path: file.path) { let _ = await model.getFileList(path: parentPath) } } - + // Friendship Quest Remix private func isInHomeFolder(_ file: FileInfo?) -> Bool { if let file { @@ -282,14 +278,13 @@ struct FilesView: View { } return false } - + var body: some View { NavigationStack { List(model.files, id: \.self, selection: $selection) { file in if file.isFolder { FolderView(file: file, depth: 0).tag(file.id) - } - else { + } else { FileView(file: file, depth: 0).tag(file.id) } } @@ -303,7 +298,7 @@ struct FilesView: View { } .contextMenu(forSelectionType: FileInfo.self) { items in let selectedFile = items.first - + Button { if let s = selectedFile, !s.isFolder { downloadFile(s) @@ -312,9 +307,9 @@ struct FilesView: View { Label("Download", systemImage: "arrow.down") } .disabled(selectedFile == nil || (selectedFile != nil && selectedFile!.isFolder)) - + Divider() - + Button { if let s = selectedFile { getFileInfo(s) @@ -323,7 +318,7 @@ struct FilesView: View { Label("Get Info", systemImage: "info.circle") } .disabled(selectedFile == nil) - + Button { if let s = selectedFile { previewFile(s) @@ -332,13 +327,13 @@ struct FilesView: View { Label("Preview", systemImage: "eye") } .disabled(selectedFile == nil || (selectedFile != nil && !selectedFile!.isPreviewable)) - + if model.access?.contains(.canDeleteFiles) == true - // Friendship Quest Remix - || isInHomeFolder(selectedFile) + // Friendship Quest Remix + || isInHomeFolder(selectedFile) { Divider() - + Button { if let s = selectedFile { Task { @@ -354,12 +349,11 @@ struct FilesView: View { guard let clickedFile = items.first else { return } - + self.selection = clickedFile if clickedFile.isFolder { clickedFile.expanded.toggle() - } - else { + } else { downloadFile(clickedFile) } } @@ -405,7 +399,7 @@ struct FilesView: View { .help("Preview") .disabled(selection == nil || selection?.isPreviewable == false) } - + ToolbarItem(placement: .primaryAction) { Button { if let selectedFile = selection { @@ -417,7 +411,7 @@ struct FilesView: View { .help("Get Info") .disabled(selection == nil) } - + ToolbarItem(placement: .primaryAction) { Button { uploadFileSelectorDisplayed = true @@ -427,7 +421,7 @@ struct FilesView: View { .help("Upload") .disabled(model.access?.contains(.canUploadFiles) != true) } - + ToolbarItem(placement: .primaryAction) { Button { if let selectedFile = selection, !selectedFile.isFolder { @@ -437,43 +431,47 @@ struct FilesView: View { Label("Download", systemImage: "arrow.down") } .help("Download") - .disabled(selection == nil || selection?.isFolder == true || model.access?.contains(.canDownloadFiles) != true) + .disabled( + selection == nil || selection?.isFolder == true + || model.access?.contains(.canDownloadFiles) != true) } } } - .sheet(item: $fileDetails ) { item in + .sheet(item: $fileDetails) { item in FileDetailsView(fd: item) } - .fileImporter(isPresented: $uploadFileSelectorDisplayed, allowedContentTypes: [.data], allowsMultipleSelection: false, onCompletion: { results in - switch results { - case .success(let fileURLS): - guard fileURLS.count > 0 else { - return - } - - let fileURL = fileURLS.first! - - print(fileURL) - - var uploadPath: [String] = [] - - if let selection = selection { - if selection.isFolder { - uploadPath = selection.path + .fileImporter( + isPresented: $uploadFileSelectorDisplayed, allowedContentTypes: [.data], + allowsMultipleSelection: false, + onCompletion: { results in + switch results { + case .success(let fileURLS): + guard fileURLS.count > 0 else { + return } - else { - uploadPath = Array<String>(selection.path) - uploadPath.removeLast() + + let fileURL = fileURLS.first! + + print(fileURL) + + var uploadPath: [String] = [] + + if let selection = selection { + if selection.isFolder { + uploadPath = selection.path + } else { + uploadPath = [String](selection.path) + uploadPath.removeLast() + } } + + print("UPLOAD PATH: \(uploadPath)") + uploadFile(file: fileURL, to: uploadPath) + + case .failure(let error): + print(error) } - - print("UPLOAD PATH: \(uploadPath)") - uploadFile(file: fileURL, to: uploadPath) - - case .failure(let error): - print(error) - } - }) + }) } } diff --git a/Hotline/macOS/HotlinePanelView.swift b/Hotline/macOS/HotlinePanelView.swift index dc58698..2617bc8 100644 --- a/Hotline/macOS/HotlinePanelView.swift +++ b/Hotline/macOS/HotlinePanelView.swift @@ -3,30 +3,30 @@ import SwiftUI struct HotlinePanelView: View { @Environment(\.openWindow) var openWindow @Environment(\.colorScheme) var colorScheme - + var body: some View { VStack(spacing: 0) { - Image(nsImage: ApplicationState.shared.activeServerBanner ?? NSImage(named: "Default Banner")!) - .interpolation(.high) - .resizable() - .scaledToFill() - .frame(width: 468, height: 60) - .frame(minWidth: 468, maxWidth: 468, minHeight: 60, maxHeight: 60) - .clipped() - .background(.black) -// .clipShape(RoundedRectangle(cornerRadius: 6.0)) -// .padding([.top, .leading, .trailing], 4) - + Image( + nsImage: ApplicationState.shared.activeServerBanner ?? NSImage(named: "Default Banner")! + ) + .interpolation(.high) + .resizable() + .scaledToFill() + .frame(width: 468, height: 60) + .frame(minWidth: 468, maxWidth: 468, minHeight: 60, maxHeight: 60) + .clipped() + .background(.black) + // .clipShape(RoundedRectangle(cornerRadius: 6.0)) + // .padding([.top, .leading, .trailing], 4) + HStack(spacing: 12) { Button { if NSEvent.modifierFlags.contains(.option) { openWindow(id: "server") - } - else { + } else { openWindow(id: "servers") } - } - label: { + } label: { Image("Section Servers") .resizable() .scaledToFit() @@ -34,11 +34,10 @@ struct HotlinePanelView: View { .buttonStyle(.plain) .frame(width: 20, height: 20) .help("Hotline Servers") - + Button { ApplicationState.shared.activeServerState?.selection = .chat - } - label: { + } label: { Image("Section Chat") .resizable() .scaledToFit() @@ -47,11 +46,10 @@ struct HotlinePanelView: View { .frame(width: 20, height: 20) .disabled(ApplicationState.shared.activeServerState == nil) .help("Public Chat") - + Button { ApplicationState.shared.activeServerState?.selection = .board - } - label: { + } label: { Image("Section Board") .resizable() .scaledToFit() @@ -60,24 +58,25 @@ struct HotlinePanelView: View { .frame(width: 20, height: 20) .disabled(ApplicationState.shared.activeServerState == nil) .help("Message Board") - + Button { ApplicationState.shared.activeServerState?.selection = .news - } - label: { + } label: { Image("Section News") .resizable() .scaledToFit() } .buttonStyle(.plain) .frame(width: 20, height: 20) - .disabled(ApplicationState.shared.activeServerState == nil || (ApplicationState.shared.activeHotline?.serverVersion ?? 0) < 151) + .disabled( + ApplicationState.shared.activeServerState == nil + || (ApplicationState.shared.activeHotline?.serverVersion ?? 0) < 151 + ) .help("News") - + Button { ApplicationState.shared.activeServerState?.selection = .files - } - label: { + } label: { Image("Section Files") .resizable() .scaledToFit() @@ -86,14 +85,13 @@ struct HotlinePanelView: View { .frame(width: 20, height: 20) .disabled(ApplicationState.shared.activeServerState == nil) .help("Files") - + Spacer() - + if ApplicationState.shared.activeHotline?.access?.contains(.canOpenUsers) == true { Button { ApplicationState.shared.activeServerState?.selection = .accounts - } - label: { + } label: { Image("Section Users") .resizable() .scaledToFit() @@ -103,7 +101,7 @@ struct HotlinePanelView: View { .disabled(ApplicationState.shared.activeServerState == nil) .help("Accounts") } - + SettingsLink(label: { Image("Section Settings") .resizable() @@ -116,29 +114,29 @@ struct HotlinePanelView: View { .padding(.top, 12) .padding(.bottom, 12) .padding([.leading, .trailing], 12) -// .background(Color.red.opacity(0.5).blendMode(.multiply)) - -// GroupBox { -// HStack(spacing: 0) { -// Text("Not Connected") -// .font(.system(size: 10.0)) -// .lineLimit(1) -// .truncationMode(.tail) -// .opacity(0.5) -// .padding(.vertical, 0.0) -// .padding(.horizontal, 4.0) -// -// Spacer() -// } -// } -// .padding([.leading, .bottom, .trailing], 4.0) + // .background(Color.red.opacity(0.5).blendMode(.multiply)) + + // GroupBox { + // HStack(spacing: 0) { + // Text("Not Connected") + // .font(.system(size: 10.0)) + // .lineLimit(1) + // .truncationMode(.tail) + // .opacity(0.5) + // .padding(.vertical, 0.0) + // .padding(.horizontal, 4.0) + // + // Spacer() + // } + // } + // .padding([.leading, .bottom, .trailing], 4.0) } -// .frame(width: 468) -// .background(colorScheme == .dark ? .black : .white) -// .background( -// VisualEffectView(material: .headerView, blendingMode: .behindWindow) -// .cornerRadius(10.0) -// ) + // .frame(width: 468) + // .background(colorScheme == .dark ? .black : .white) + // .background( + // VisualEffectView(material: .headerView, blendingMode: .behindWindow) + // .cornerRadius(10.0) + // ) } } diff --git a/Hotline/macOS/MessageBoardEditorView.swift b/Hotline/macOS/MessageBoardEditorView.swift index 474384e..9026c6f 100644 --- a/Hotline/macOS/MessageBoardEditorView.swift +++ b/Hotline/macOS/MessageBoardEditorView.swift @@ -9,28 +9,28 @@ struct MessageBoardEditorView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) private var dismiss @Environment(Hotline.self) private var model: Hotline - + @State private var text: String = "" @State private var sending: Bool = false - + @FocusState private var focusedField: FocusFields? - + func sendPost() async { sending = true - + let cleanedText = text.trimmingCharacters(in: .whitespacesAndNewlines) - + model.postToMessageBoard(text: cleanedText) let _ = await model.getMessageBoard() - -// let success = await model.postNewsArticle(title: title, body: text, at: path, parentID: parentID) -// if success { -// await model.getNewsList(at: path) -// } - + + // let success = await model.postNewsArticle(title: title, body: text, at: path, parentID: parentID) + // if success { + // await model.getNewsList(at: path) + // } + sending = false } - + var body: some View { VStack(alignment: .leading, spacing: 0) { HStack(alignment: .center, spacing: 0) { @@ -45,28 +45,27 @@ struct MessageBoardEditorView: View { } .buttonStyle(.plain) .frame(width: 16, height: 16) - + Spacer() - -// Image("Message Board Post") -// .resizable() -// .scaledToFit() -// .frame(width: 16, height: 16) -// .padding(.trailing, 6) - + + // Image("Message Board Post") + // .resizable() + // .scaledToFit() + // .frame(width: 16, height: 16) + // .padding(.trailing, 6) + Text("New Post") .fontWeight(.semibold) .lineLimit(1) .truncationMode(.middle) - + Spacer() - + if sending { ProgressView() .controlSize(.small) .frame(width: 22, height: 22) - } - else { + } else { Button { sending = true model.postToMessageBoard(text: text) @@ -82,7 +81,9 @@ struct MessageBoardEditorView: View { .resizable() .renderingMode(.template) .scaledToFit() - .foregroundColor(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .secondary : .accentColor) + .foregroundColor( + text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + ? .secondary : .accentColor) } .buttonStyle(.plain) .frame(width: 22, height: 22) @@ -92,9 +93,9 @@ struct MessageBoardEditorView: View { } .frame(maxWidth: .infinity) .padding() - + Divider() - + BetterTextEditor(text: $text) .betterEditorFont(NSFont.systemFont(ofSize: 14.0)) .betterEditorAutomaticSpellingCorrection(true) @@ -104,7 +105,10 @@ struct MessageBoardEditorView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .focused($focusedField, equals: .body) } - .frame(minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 300, idealHeight: 500, maxHeight: .infinity) + .frame( + minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 300, idealHeight: 500, + maxHeight: .infinity + ) .background(Color(nsColor: .textBackgroundColor)) .presentationCompactAdaptation(.sheet) .onAppear { diff --git a/Hotline/macOS/MessageBoardView.swift b/Hotline/macOS/MessageBoardView.swift index f870b0c..115e4e3 100644 --- a/Hotline/macOS/MessageBoardView.swift +++ b/Hotline/macOS/MessageBoardView.swift @@ -2,10 +2,10 @@ import SwiftUI struct MessageBoardView: View { @Environment(Hotline.self) private var model: Hotline - + @State private var composerDisplayed: Bool = false @State private var composerText: String = "" - + var body: some View { NavigationStack { if model.access?.contains(.canReadMessageBoard) != false { @@ -38,8 +38,7 @@ struct MessageBoardView: View { } } .background(Color(nsColor: .textBackgroundColor)) - } - else { + } else { ZStack(alignment: .center) { Text("No Message Board") .font(.title) @@ -54,36 +53,36 @@ struct MessageBoardView: View { MessageBoardEditorView() .frame(maxWidth: .infinity, maxHeight: .infinity) .frame(idealWidth: 450, idealHeight: 350) -// RichTextEditor(text: $composerText) -// .richEditorFont(NSFont.systemFont(ofSize: 16.0)) -// .richEditorAutomaticDashSubstitution(false) -// .richEditorAutomaticQuoteSubstitution(false) -// .richEditorAutomaticSpellingCorrection(false) -// .background(Color(nsColor: .textBackgroundColor)) -// .frame(maxWidth: .infinity, maxHeight: .infinity) -// .frame(idealWidth: 450, idealHeight: 350) -// .toolbar { -// ToolbarItem(placement: .cancellationAction) { -// Button("Cancel") { -// composerDisplayed.toggle() -// } -// } -// -// ToolbarItem(placement: .primaryAction) { -// Button("Post") { -// composerDisplayed.toggle() -// let text = composerText -// composerText = "" -// model.postToMessageBoard(text: text) -// Task { -// await model.getMessageBoard() -// } -// } -// } -// } + // RichTextEditor(text: $composerText) + // .richEditorFont(NSFont.systemFont(ofSize: 16.0)) + // .richEditorAutomaticDashSubstitution(false) + // .richEditorAutomaticQuoteSubstitution(false) + // .richEditorAutomaticSpellingCorrection(false) + // .background(Color(nsColor: .textBackgroundColor)) + // .frame(maxWidth: .infinity, maxHeight: .infinity) + // .frame(idealWidth: 450, idealHeight: 350) + // .toolbar { + // ToolbarItem(placement: .cancellationAction) { + // Button("Cancel") { + // composerDisplayed.toggle() + // } + // } + // + // ToolbarItem(placement: .primaryAction) { + // Button("Post") { + // composerDisplayed.toggle() + // let text = composerText + // composerText = "" + // model.postToMessageBoard(text: text) + // Task { + // await model.getMessageBoard() + // } + // } + // } + // } } .toolbar { - ToolbarItem(placement:.primaryAction) { + ToolbarItem(placement: .primaryAction) { Button { composerDisplayed.toggle() } label: { diff --git a/Hotline/macOS/MessageView.swift b/Hotline/macOS/MessageView.swift index 0a8b071..f62f922 100644 --- a/Hotline/macOS/MessageView.swift +++ b/Hotline/macOS/MessageView.swift @@ -3,19 +3,19 @@ import SwiftUI struct MessageView: View { @Environment(Hotline.self) private var model: Hotline @Environment(\.colorScheme) private var colorScheme - + @State private var input: String = "" @State private var scrollPos: Int? @State private var contentHeight: CGFloat = 0 @Namespace private var bottomID @FocusState private var focusedField: FocusedField? - + var userID: UInt16 - + var body: some View { ScrollViewReader { reader in VStack(alignment: .leading, spacing: 0) { - + // MARK: Scroll View GeometryReader { gm in ScrollView(.vertical) { @@ -25,17 +25,27 @@ struct MessageView: View { if msg.direction == .outgoing { Spacer() } - + Text(LocalizedStringKey(msg.text)) .lineSpacing(4) .multilineTextAlignment(.leading) .textSelection(.enabled) - .tint(msg.direction == .outgoing ? Color("Outgoing Message Link") : Color("Link Color")) - .foregroundStyle(msg.direction == .outgoing ? Color("Outgoing Message Text") : Color("Incoming Message Text")) + .tint( + msg.direction == .outgoing + ? Color("Outgoing Message Link") : Color("Link Color") + ) + .foregroundStyle( + msg.direction == .outgoing + ? Color("Outgoing Message Text") : Color("Incoming Message Text") + ) .padding(EdgeInsets(top: 10, leading: 14, bottom: 10, trailing: 14)) - .background(msg.direction == .outgoing ? Color("Outgoing Message Background") : Color("Incoming Message Background")) + .background( + msg.direction == .outgoing + ? Color("Outgoing Message Background") + : Color("Incoming Message Background") + ) .clipShape(RoundedRectangle(cornerRadius: 16)) - + if msg.direction == .incoming { Spacer() } @@ -44,7 +54,7 @@ struct MessageView: View { } } .padding() - + VStack(spacing: 0) {}.id(bottomID) } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -61,10 +71,10 @@ struct MessageView: View { reader.scrollTo(bottomID, anchor: .bottom) } } - + // MARK: Input Divider Divider() - + // MARK: Input Bar HStack(alignment: .lastTextBaseline, spacing: 0) { let user = model.users.first(where: { $0.id == userID }) diff --git a/Hotline/macOS/NewsEditorView.swift b/Hotline/macOS/NewsEditorView.swift index f69c846..9a7242e 100644 --- a/Hotline/macOS/NewsEditorView.swift +++ b/Hotline/macOS/NewsEditorView.swift @@ -10,31 +10,32 @@ struct NewsEditorView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) private var dismiss @Environment(Hotline.self) private var model: Hotline - + let editorTitle: String let isReply: Bool let path: [String] let parentID: UInt32 - + @State var title: String = "" @State private var text: String = "" @State private var sending: Bool = false - + @FocusState private var focusedField: FocusFields? - + func sendArticle() async -> Bool { sending = true - - let success = await model.postNewsArticle(title: title, body: text, at: path, parentID: parentID) + + let success = await model.postNewsArticle( + title: title, body: text, at: path, parentID: parentID) if success { await model.getNewsList(at: path) } - + sending = false - + return success } - + var body: some View { VStack(alignment: .leading, spacing: 0) { HStack(alignment: .center, spacing: 0) { @@ -49,9 +50,9 @@ struct NewsEditorView: View { } .buttonStyle(.plain) .frame(width: 16, height: 16) - + Spacer() - + if !isReply { Image("News Category") .resizable() @@ -59,20 +60,19 @@ struct NewsEditorView: View { .frame(width: 16, height: 16) .padding(.trailing, 6) } - + Text(editorTitle) .fontWeight(.semibold) .lineLimit(1) .truncationMode(.middle) - + Spacer() - + if sending { ProgressView() .controlSize(.small) .frame(width: 22, height: 22) - } - else { + } else { Button { Task { if await sendArticle() { @@ -94,7 +94,7 @@ struct NewsEditorView: View { } .frame(maxWidth: .infinity) .padding([.leading, .top, .trailing]) - + TextField("Title", text: $title, axis: .vertical) .textFieldStyle(.plain) .lineLimit(3) @@ -107,9 +107,9 @@ struct NewsEditorView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) .padding() .focused($focusedField, equals: .title) - + Divider() - + BetterTextEditor(text: $text) .betterEditorFont(NSFont.monospacedSystemFont(ofSize: 14.0, weight: .regular)) .betterEditorAutomaticSpellingCorrection(true) @@ -117,10 +117,10 @@ struct NewsEditorView: View { .background(Color(nsColor: .textBackgroundColor)) .frame(maxWidth: .infinity, maxHeight: .infinity) .focused($focusedField, equals: .body) - + HStack(alignment: .center) { Spacer() - + Text(String("**bold** _italics_ [link name](url) ")) .foregroundStyle(.secondary) .font(.caption) @@ -128,21 +128,23 @@ struct NewsEditorView: View { .lineLimit(1) .truncationMode(.middle) .padding() - + Spacer() } .frame(maxWidth: .infinity) .background(.tertiary.opacity(0.15)) } - .frame(minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 300, idealHeight: 500, maxHeight: .infinity) + .frame( + minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 300, idealHeight: 500, + maxHeight: .infinity + ) .background(Color(nsColor: .textBackgroundColor)) .presentationCompactAdaptation(.sheet) .toolbarTitleDisplayMode(.inlineLarge) .onAppear { if !title.isEmpty { focusedField = .body - } - else { + } else { focusedField = .title } } diff --git a/Hotline/macOS/NewsItemView.swift b/Hotline/macOS/NewsItemView.swift index fc20e61..ff7495b 100644 --- a/Hotline/macOS/NewsItemView.swift +++ b/Hotline/macOS/NewsItemView.swift @@ -2,10 +2,10 @@ import SwiftUI struct NewsItemView: View { @Environment(Hotline.self) private var model: Hotline - + var news: NewsInfo let depth: Int - + static var dateFormatter: DateFormatter = { var dateFormatter = DateFormatter() dateFormatter.dateStyle = .long @@ -13,7 +13,7 @@ struct NewsItemView: View { dateFormatter.timeZone = .gmt return dateFormatter }() - + static var relativeDateFormatter: RelativeDateTimeFormatter = { var formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .full @@ -21,7 +21,7 @@ struct NewsItemView: View { formatter.formattingContext = .listItem return formatter }() - + var body: some View { HStack(alignment: .center, spacing: 0) { if news.expandable { @@ -38,18 +38,17 @@ struct NewsItemView: View { .frame(width: 10) .padding(.leading, 4) .padding(.trailing, 8) - } - else { + } else { Spacer() .frame(width: 10) .padding(.leading, 4) .padding(.trailing, 8) } - + // Tree indent Spacer() .frame(width: (CGFloat(depth) * 22)) - + switch news.type { case .category: Image("News Category") @@ -64,60 +63,61 @@ struct NewsItemView: View { case .article: EmptyView() } - + Text(news.name) - .fontWeight((news.type == .bundle || news.type == .category || !news.read) ? .semibold : .regular) + .fontWeight( + (news.type == .bundle || news.type == .category || !news.read) ? .semibold : .regular + ) .lineLimit(1) .truncationMode(.tail) - + if news.type == .article && news.articleUsername != nil { Text(news.articleUsername!) .foregroundStyle(.secondary) .lineLimit(1) .padding(.leading, 8) } - + Spacer() - + if news.type == .category && news.count > 0 { Text("^[\(news.count) Post](inflect: true)") .lineLimit(1) .foregroundStyle(.secondary) .padding(.trailing, 8) - } - else if news.type == .bundle && news.count > 0 { + } else if news.type == .bundle && news.count > 0 { Text("^[\(news.count) Category](inflect: true)") .lineLimit(1) .foregroundStyle(.secondary) .padding(.trailing, 8) } -// if news.type == .bundle { -// Text("\(news.count)") -// .lineLimit(1) -// .foregroundStyle(.tertiary) -// .padding(.trailing, 8) - -// ZStack { -// Text("^[\(news.count) \(news.type == .bundle ? "Category" : "Post")](inflect: true)") -// Text("\(news.count)") -// .foregroundStyle(.clear) -//// .font(.caption) -// .lineLimit(1) -// .padding([.leading, .trailing], 8) -// .padding([.top, .bottom], 2) -// .background(.secondary) -// .clipShape(Capsule()) -// -// Text("\(news.count)") -// .foregroundStyle(.white) -//// .font(.caption) -// .lineLimit(1) -// .padding([.leading, .trailing], 8) -// .padding([.top, .bottom], 2) -// .blendMode(.destinationOut) -// } -// .drawingGroup(opaque: false) -// } + // if news.type == .bundle { + // Text("\(news.count)") + // .lineLimit(1) + // .foregroundStyle(.tertiary) + // .padding(.trailing, 8) + + // ZStack { + // Text("^[\(news.count) \(news.type == .bundle ? "Category" : "Post")](inflect: true)") + // Text("\(news.count)") + // .foregroundStyle(.clear) + //// .font(.caption) + // .lineLimit(1) + // .padding([.leading, .trailing], 8) + // .padding([.top, .bottom], 2) + // .background(.secondary) + // .clipShape(Capsule()) + // + // Text("\(news.count)") + // .foregroundStyle(.white) + //// .font(.caption) + // .lineLimit(1) + // .padding([.leading, .trailing], 8) + // .padding([.top, .bottom], 2) + // .blendMode(.destinationOut) + // } + // .drawingGroup(opaque: false) + // } else if news.type == .article && news.articleUsername != nil { if let d = news.articleDate { Text(NewsItemView.relativeDateFormatter.localizedString(for: d, relativeTo: Date.now)) @@ -132,12 +132,12 @@ struct NewsItemView: View { guard news.expanded, news.type == .bundle || news.type == .category else { return } - + Task { await model.getNewsList(at: news.path) } } - + if news.expanded { ForEach(news.children.reversed(), id: \.self) { childNews in NewsItemView(news: childNews, depth: self.depth + 1).tag(childNews.id) @@ -147,6 +147,11 @@ struct NewsItemView: View { } #Preview { - NewsItemView(news: NewsInfo(hotlineNewsArticle: HotlineNewsArticle(id: 0, parentID: 0, flags: 0, title: "Title", username: "username", date: Date.now, flavors: [("", 1)], path: ["Guest"])), depth: 0) - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) + NewsItemView( + news: NewsInfo( + hotlineNewsArticle: HotlineNewsArticle( + id: 0, parentID: 0, flags: 0, title: "Title", username: "username", date: Date.now, + flavors: [("", 1)], path: ["Guest"])), depth: 0 + ) + .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) } diff --git a/Hotline/macOS/NewsView.swift b/Hotline/macOS/NewsView.swift index 91bf2fe..d710c62 100644 --- a/Hotline/macOS/NewsView.swift +++ b/Hotline/macOS/NewsView.swift @@ -1,20 +1,21 @@ -import SwiftUI import MarkdownUI import SplitView +import SwiftUI struct NewsView: View { @Environment(Hotline.self) private var model: Hotline @Environment(\.openWindow) private var openWindow @Environment(\.colorScheme) private var colorScheme - + @State private var selection: NewsInfo? @State private var articleText: String? @State private var splitHidden = SideHolder(.bottom) - @State private var splitFraction = FractionHolder.usingUserDefaults(0.25, key: "News Split Fraction") + @State private var splitFraction = FractionHolder.usingUserDefaults( + 0.25, key: "News Split Fraction") @State private var editorOpen: Bool = false @State private var replyOpen: Bool = false @State private var loading: Bool = false - + var body: some View { Group { if model.serverVersion < 151 { @@ -28,15 +29,13 @@ struct NewsView: View { .font(.system(size: 13)) } .padding() - } - else { + } else { NavigationStack { VSplit( top: { if !model.newsLoaded { loadingIndicator - } - else if model.news.isEmpty { + } else if model.news.isEmpty { ZStack(alignment: .center) { Text("No News") .font(.title) @@ -45,8 +44,7 @@ struct NewsView: View { .padding() } .frame(maxWidth: .infinity) - } - else { + } else { newsBrowser } }, @@ -57,7 +55,10 @@ struct NewsView: View { .fraction(splitFraction) .constraints(minPFraction: 0.1, minSFraction: 0.3) .hide(splitHidden) - .styling(color: colorScheme == .dark ? .black : Splitter.defaultColor, inset: 0, visibleThickness: 0.5, invisibleThickness: 5, hideSplitter: true) + .styling( + color: colorScheme == .dark ? .black : Splitter.defaultColor, inset: 0, + visibleThickness: 0.5, invisibleThickness: 5, hideSplitter: true + ) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(nsColor: .textBackgroundColor)) } @@ -75,21 +76,24 @@ struct NewsView: View { if let selection = selection { switch selection.type { case .article, .category: - NewsEditorView(editorTitle: selection.path.last ?? "New Post", isReply: false, path: selection.path, parentID: 0) + NewsEditorView( + editorTitle: selection.path.last ?? "New Post", isReply: false, path: selection.path, + parentID: 0) default: EmptyView() } - } - else { + } else { EmptyView() } } .sheet(isPresented: $replyOpen) { } content: { if let selection = selection, selection.type == .article { - NewsEditorView(editorTitle: "Reply to \(selection.articleUsername ?? "Post")", isReply: true, path: selection.path, parentID: UInt32(selection.articleID!), title: selection.name.replyToString()) - } - else { + NewsEditorView( + editorTitle: "Reply to \(selection.articleUsername ?? "Post")", isReply: true, + path: selection.path, parentID: UInt32(selection.articleID!), + title: selection.name.replyToString()) + } else { EmptyView() } } @@ -105,7 +109,7 @@ struct NewsView: View { .help("New Post") .disabled(selection?.type != .category && selection?.type != .article) } - + ToolbarItem(placement: .primaryAction) { Button { if selection?.type == .article { @@ -117,7 +121,7 @@ struct NewsView: View { .help("Reply to Post") .disabled(selection?.type != .article) } - + ToolbarItem(placement: .primaryAction) { Button { loading = true @@ -126,8 +130,7 @@ struct NewsView: View { await model.getNewsList(at: selectionPath) loading = false } - } - else { + } else { Task { await model.getNewsList() loading = false @@ -141,7 +144,7 @@ struct NewsView: View { } } } - + var newsBrowser: some View { List(model.news, id: \.self, selection: $selection) { newsItem in NewsItemView(news: newsItem, depth: 0).tag(newsItem.id) @@ -152,23 +155,27 @@ struct NewsView: View { .alternatingRowBackgrounds(.enabled) .contextMenu(forSelectionType: NewsInfo.self) { items in let selectedItem = items.first - + Button { if selectedItem?.type == .article { replyOpen = true } } label: { - Label("Reply to \(selectedItem?.articleUsername ?? "Post")", systemImage: "arrowshape.turn.up.left") + Label( + "Reply to \(selectedItem?.articleUsername ?? "Post")", + systemImage: "arrowshape.turn.up.left") } .disabled(selectedItem == nil || selectedItem?.type != .article) - + } primaryAction: { items in guard let clickedNews = items.first else { return } - + self.selection = clickedNews - if clickedNews.type == .bundle || clickedNews.type == .category || clickedNews.children.count > 0 { + if clickedNews.type == .bundle || clickedNews.type == .category + || clickedNews.children.count > 0 + { clickedNews.expanded.toggle() } } @@ -177,9 +184,12 @@ struct NewsView: View { if let article = selection, article.type == .article { article.read = true if let articleFlavor = article.articleFlavors?.first, - let articleID = article.articleID { + let articleID = article.articleID + { Task { - if let articleText = await self.model.getNewsArticle(id: articleID, at: article.path, flavor: articleFlavor) { + if let articleText = await self.model.getNewsArticle( + id: articleID, at: article.path, flavor: articleFlavor) + { self.articleText = articleText } } @@ -188,10 +198,9 @@ struct NewsView: View { self.splitHidden.side = nil } } - + } - } - else { + } else { if self.splitHidden.side != .bottom { withAnimation(.easeOut(duration: 0.25)) { self.splitHidden.side = .bottom @@ -214,7 +223,7 @@ struct NewsView: View { return .ignored } } - + var loadingIndicator: some View { VStack { HStack { @@ -226,7 +235,7 @@ struct NewsView: View { } .frame(maxWidth: .infinity) } - + var articleViewer: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { @@ -248,14 +257,14 @@ struct NewsView: View { .padding(.bottom, 16) } } - + Divider() - + Text(selection.name).font(.title) .textSelection(.enabled) .padding(.bottom, 8) .padding(.top, 16) - + if let newsText = self.articleText { Markdown(newsText) .markdownTheme(.basic) @@ -263,16 +272,15 @@ struct NewsView: View { .lineSpacing(6) .padding(.top, 16) } - } - else { + } else { HStack(alignment: .center) { Spacer() HStack(alignment: .center, spacing: 8) { -// Image(systemName: "doc.append") -// .resizable() -// .scaledToFit() -// .foregroundStyle(.tertiary) -// .frame(width: 16, height: 16) + // Image(systemName: "doc.append") + // .resizable() + // .scaledToFit() + // .foregroundStyle(.tertiary) + // .frame(width: 16, height: 16) Text("Select a news post to read") .foregroundStyle(.tertiary) .font(.system(size: 13)) diff --git a/Hotline/macOS/ServerAgreementView.swift b/Hotline/macOS/ServerAgreementView.swift index a1f96d9..77e9e15 100644 --- a/Hotline/macOS/ServerAgreementView.swift +++ b/Hotline/macOS/ServerAgreementView.swift @@ -1,13 +1,13 @@ import SwiftUI -fileprivate let MAX_AGREEMENT_HEIGHT: CGFloat = 280 +private let MAX_AGREEMENT_HEIGHT: CGFloat = 280 struct ServerAgreementView: View { let text: String - + @State private var expandable: Bool = false @State private var expanded: Bool = false - + var body: some View { ScrollView(.vertical) { HStack(alignment: .top) { @@ -24,8 +24,7 @@ struct ServerAgreementView: View { Color.clear.onAppear { if geometry.size.height > MAX_AGREEMENT_HEIGHT { expandable = true - } - else { + } else { expandable = false } } @@ -37,41 +36,42 @@ struct ServerAgreementView: View { .scrollIndicators(.never) .frame(maxWidth: .infinity, maxHeight: (expandable && expanded) ? nil : MAX_AGREEMENT_HEIGHT) .scrollBounceBehavior(.basedOnSize) -#if os(iOS) - .background(Color("Agreement Background")) -#elseif os(macOS) - .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow)) -#endif + #if os(iOS) + .background(Color("Agreement Background")) + #elseif os(macOS) + .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow)) + #endif .overlay( ZStack(alignment: .bottomTrailing) { Group { if !expandable || expanded { EmptyView() - } - else { - Button(action: { - withAnimation(.easeOut(duration: 0.15)) { - expanded = true + } else { + Button( + action: { + withAnimation(.easeOut(duration: 0.15)) { + expanded = true + } + }, + label: { + Color.black + .opacity(0.00001) + .frame(width: 32, height: 32) + .overlay( + Image(systemName: "arrow.up.left.and.arrow.down.right") + .resizable() + .scaledToFit() + .fontWeight(.semibold) + .frame(width: 12, height: 12) + .foregroundColor(.primary.opacity(0.8)), alignment: .center) } - }, label: { - Color.black - .opacity(0.00001) - .frame(width: 32, height: 32) - .overlay( - Image(systemName: "arrow.up.left.and.arrow.down.right") - .resizable() - .scaledToFit() - .fontWeight(.semibold) - .frame(width: 12, height: 12) - .foregroundColor(.primary.opacity(0.8)) - , alignment: .center) - }) + ) .buttonStyle(.plain) .help("Expand Server Agreement") } } - } - , alignment: .bottomTrailing) + }, alignment: .bottomTrailing + ) .clipShape(RoundedRectangle(cornerRadius: 8)) } } diff --git a/Hotline/macOS/ServerMessageView.swift b/Hotline/macOS/ServerMessageView.swift index 8413a87..98f4043 100644 --- a/Hotline/macOS/ServerMessageView.swift +++ b/Hotline/macOS/ServerMessageView.swift @@ -2,7 +2,7 @@ import SwiftUI struct ServerMessageView: View { let message: String - + var body: some View { HStack(alignment: .center, spacing: 8) { Image("Server Message") @@ -19,11 +19,11 @@ struct ServerMessageView: View { } .padding() .frame(maxWidth: .infinity) -#if os(iOS) - .background(Color("Agreement Background")) -#elseif os(macOS) - .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow)) -#endif + #if os(iOS) + .background(Color("Agreement Background")) + #elseif os(macOS) + .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow)) + #endif .clipShape(RoundedRectangle(cornerRadius: 8)) } } diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift index 1e53e49..3a25925 100644 --- a/Hotline/macOS/ServerView.swift +++ b/Hotline/macOS/ServerView.swift @@ -5,11 +5,11 @@ import UniformTypeIdentifiers 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 } @@ -29,18 +29,18 @@ struct ServerMenuItem: Identifiable, Hashable { let type: ServerNavigationType let name: String let image: String - + init(type: ServerNavigationType, name: String, image: String) { self.id = UUID() self.type = type self.name = name self.image = image } - + func hash(into hasher: inout Hasher) { hasher.combine(id) } - + static func == (lhs: ServerMenuItem, rhs: ServerMenuItem) -> Bool { switch lhs.type { case .user(let lhsUID): @@ -59,23 +59,23 @@ struct ServerMenuItem: Identifiable, Hashable { struct ListItemView: View { @Environment(\.controlActiveState) private var controlActiveState - + let icon: String? let title: String let unread: Bool - + var body: some View { HStack(spacing: 5) { if let i = icon { Image(i) .resizable() - // .renderingMode(.template) + // .renderingMode(.template) .scaledToFit() .frame(width: 20, height: 20) -// .padding(.leading, 2) + // .padding(.leading, 2) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) } - + Text(title) .lineLimit(1) .truncationMode(.tail) @@ -103,7 +103,7 @@ extension FocusedValues { get { self[ActiveHotlineModelFocusedValueKey.self] } set { self[ActiveHotlineModelFocusedValueKey.self] = newValue } } - + var activeServerState: ServerState? { get { self[ActiveServerStateFocusedValueKey.self] } set { self[ActiveServerStateFocusedValueKey.self] = newValue } @@ -127,7 +127,7 @@ enum ServerNavigationType: Identifiable, Hashable, Equatable { return String(userID) } } - + case chat case news case board @@ -142,10 +142,11 @@ struct ServerView: View { @Environment(\.controlActiveState) private var controlActiveState @Environment(\.scenePhase) private var scenePhase @Environment(\.modelContext) private var modelContext - + @State private var reconnectTimer: Task<Void, Never>? @State private var shouldReconnect: Bool = false - @State private var model: Hotline = Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient()) + @State private var model: Hotline = Hotline( + trackerClient: HotlineTrackerClient(), client: HotlineClient()) @State private var state: ServerState = ServerState(selection: .chat) @State private var agreementShown: Bool = false @State private var connectAddress: String = "" @@ -156,7 +157,7 @@ struct ServerView: View { @State private var autoconnect: Bool = false @Binding var server: Server - + static var menuItems: [ServerMenuItem] = [ ServerMenuItem(type: .chat, name: "Chat", image: "Section Chat"), ServerMenuItem(type: .board, name: "Board", image: "Section Board"), @@ -164,28 +165,27 @@ struct ServerView: View { ServerMenuItem(type: .files, name: "Files", image: "Section Files"), ServerMenuItem(type: .accounts, name: "Accounts", image: "Section Users"), ] - + static var classicMenuItems: [ServerMenuItem] = [ ServerMenuItem(type: .chat, name: "Chat", image: "Section Chat"), ServerMenuItem(type: .board, name: "Board", image: "Section Board"), ServerMenuItem(type: .files, name: "Files", image: "Section Files"), ] - + enum FocusFields { case address case login case password } - + @FocusState private var focusedField: FocusFields? - + var body: some View { Group { if model.status == .disconnected { connectForm .navigationTitle("Connect to Server") - } - else if model.status != .loggedIn { + } else if model.status != .loggedIn { HStack { Image("Hotline") .resizable() @@ -195,7 +195,7 @@ struct ServerView: View { .frame(width: 18) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) .padding(.trailing, 4) - + ProgressView(value: connectionStatusToProgress(status: model.status)) { Text(connectionStatusToLabel(status: model.status)) } @@ -204,8 +204,7 @@ struct ServerView: View { .frame(maxWidth: 300) .padding() .navigationTitle("Connecting to Server") - } - else { + } else { serverView .environment(model) .onChange(of: Prefs.shared.userIconID) { sendPreferences() } @@ -217,8 +216,8 @@ struct ServerView: View { .toolbar { ToolbarItem(placement: .navigation) { Image("Server Large") -// .renderingMode(.template) - + // .renderingMode(.template) + .resizable() .scaledToFit() .frame(width: 28) @@ -251,7 +250,7 @@ struct ServerView: View { connectAddress = server.address connectLogin = server.login connectPassword = server.password - + // Connect to server automatically unless the option key is held down. if !NSEvent.modifierFlags.contains(.option) { connectToServer() @@ -260,16 +259,16 @@ struct ServerView: View { .focusedSceneValue(\.activeHotlineModel, model) .focusedSceneValue(\.activeServerState, state) } - + private func startReconnectTimer() { - reconnectTimer = Task { - while !Task.isCancelled { - connectToServer() - try? await Task.sleep(for: .seconds(5)) - } + reconnectTimer = Task { + while !Task.isCancelled { + connectToServer() + try? await Task.sleep(for: .seconds(5)) } } - + } + var connectForm: some View { VStack(alignment: .center) { GroupBox { @@ -279,12 +278,14 @@ struct ServerView: View { Text("Address:") } .focused($focusedField, equals: .address) - - Text("Type the address of the Hotline server you would like to connect to. If you have an account on that server, type your login and password too.") - .font(.caption) - .foregroundStyle(.secondary) - .padding(.bottom, 4) - + + Text( + "Type the address of the Hotline server you would like to connect to. If you have an account on that server, type your login and password too." + ) + .font(.caption) + .foregroundStyle(.secondary) + .padding(.bottom, 4) + TextField(text: $connectLogin, prompt: Text("Optional")) { Text("Login:") } @@ -296,7 +297,7 @@ struct ServerView: View { } .textFieldStyle(.roundedBorder) .controlSize(.large) - + HStack { Button("Save...") { if !connectAddress.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { @@ -307,28 +308,28 @@ struct ServerView: View { .controlSize(.regular) .buttonStyle(.automatic) .help("Bookmark server") - + Spacer() - + Button("Cancel") { dismiss() } .controlSize(.regular) .buttonStyle(.automatic) .keyboardShortcut(.cancelAction) - + Button("Connect") { Task { connectToServer() } } - + .controlSize(.regular) .buttonStyle(.automatic) .keyboardShortcut(.defaultAction) } .padding(.top, 8) - + } .padding() .onChange(of: connectAddress) { @@ -368,7 +369,7 @@ struct ServerView: View { connectName = "" } } - + ToolbarItem(placement: .confirmationAction) { Button("Save") { let name = String(connectName.trimmingCharacters(in: .whitespacesAndNewlines)) @@ -379,9 +380,11 @@ struct ServerView: View { let (host, port) = await Server.parseServerAddressAndPort(connectAddress) let login: String? = await connectLogin.isEmpty ? nil : connectLogin let password: String? = await connectPassword.isEmpty ? nil : connectPassword - + if !host.isEmpty { - let newBookmark = await Bookmark(type: .server, name: name, address: host, port: port, login: login, password: password, autoconnect: autoconnect) + let newBookmark = await Bookmark( + type: .server, name: name, address: host, port: port, login: login, + password: password, autoconnect: autoconnect) await MainActor.run { Bookmark.add(newBookmark, context: modelContext) } @@ -396,38 +399,39 @@ struct ServerView: View { } } } - + var navigationList: some View { List(selection: $state.selection) { // Don't show news on older servers. - ForEach(model.serverVersion < 151 ? ServerView.classicMenuItems : ServerView.menuItems) { menuItem in + ForEach(model.serverVersion < 151 ? ServerView.classicMenuItems : ServerView.menuItems) { + menuItem in if menuItem.type == .chat { - ListItemView(icon: menuItem.image, title: menuItem.name, unread: model.unreadPublicChat).tag(menuItem.type) - } - else if menuItem.type == .accounts { + ListItemView(icon: menuItem.image, title: menuItem.name, unread: model.unreadPublicChat) + .tag(menuItem.type) + } else if menuItem.type == .accounts { if model.access?.contains(.canOpenUsers) == true { - ListItemView(icon: menuItem.image, title: menuItem.name, unread: false).tag(menuItem.type) + ListItemView(icon: menuItem.image, title: menuItem.name, unread: false).tag( + menuItem.type) } - } - else { + } else { ListItemView(icon: menuItem.image, title: menuItem.name, unread: false).tag(menuItem.type) } } - + if model.transfers.count > 0 { Divider() - + self.transfersSection } - + if model.users.count > 0 { Divider() - + self.usersSection } } .onChange(of: state.selection) { - switch(state.selection) { + switch state.selection { case .chat: model.markPublicChatAsRead() case .user(let userID): @@ -437,118 +441,116 @@ struct ServerView: View { } } } - + var transfersSection: some View { -// Section("Transfers") { - ForEach(model.transfers) { transfer in - TransferItemView(transfer: transfer) - } -// } + // Section("Transfers") { + ForEach(model.transfers) { transfer in + TransferItemView(transfer: transfer) + } + // } } - + var usersSection: some View { -// Section("\(model.users.count) Online") { - ForEach(model.users) { user in - HStack(spacing: 5) { - if let iconImage = Hotline.getClassicIcon(Int(user.iconID)) { - Image(nsImage: iconImage) - .frame(width: 16, height: 16) - .padding(.leading, 2) - .padding(.trailing, 2) - } - else { - Image("User") - .frame(width: 16, height: 16) - .padding(.leading, 2) - .padding(.trailing, 2) - } - - Text(user.name) - .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary) - - Spacer() - - if model.hasUnreadInstantMessages(userID: user.id) { - Circle() - .frame(width: 6, height: 6) - .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary.opacity(0.5)) - .padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 2)) - } + // Section("\(model.users.count) Online") { + ForEach(model.users) { user in + HStack(spacing: 5) { + if let iconImage = Hotline.getClassicIcon(Int(user.iconID)) { + Image(nsImage: iconImage) + .frame(width: 16, height: 16) + .padding(.leading, 2) + .padding(.trailing, 2) + } else { + Image("User") + .frame(width: 16, height: 16) + .padding(.leading, 2) + .padding(.trailing, 2) + } + + Text(user.name) + .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary) + + Spacer() + + if model.hasUnreadInstantMessages(userID: user.id) { + Circle() + .frame(width: 6, height: 6) + .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary.opacity(0.5)) + .padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 2)) } - .opacity(user.isIdle ? 0.5 : 1.0) - .opacity(controlActiveState == .inactive ? 0.5 : 1.0) - .tag(ServerNavigationType.user(userID: user.id)) } -// } + .opacity(user.isIdle ? 0.5 : 1.0) + .opacity(controlActiveState == .inactive ? 0.5 : 1.0) + .tag(ServerNavigationType.user(userID: user.id)) + } + // } } - + var serverView: some View { NavigationSplitView { self.navigationList .frame(maxWidth: .infinity) .navigationSplitViewColumnWidth(min: 150, ideal: 200, max: 500) } detail: { - switch state.selection { - case .chat: - ChatView() - .navigationTitle(model.serverTitle) - .navigationSubtitle("Public Chat") - .navigationSplitViewColumnWidth(min: 250, ideal: 500) - case .news: - NewsView() - .navigationTitle(model.serverTitle) - .navigationSubtitle("Newsgroups") - .navigationSplitViewColumnWidth(min: 250, ideal: 500) - case .board: - MessageBoardView() - .navigationTitle(model.serverTitle) - .navigationSubtitle("Message Board") - .navigationSplitViewColumnWidth(min: 250, ideal: 500) - case .files: - FilesView() - .navigationTitle(model.serverTitle) - .navigationSubtitle("Shared Files") - .navigationSplitViewColumnWidth(min: 250, ideal: 500) - case .accounts: - AccountManagerView() - .navigationTitle(model.serverTitle) - .navigationSubtitle("Accounts") - .navigationSplitViewColumnWidth(min: 250, ideal: 500) - case .user(let userID): - let user = model.users.first(where: { $0.id == userID }) - MessageView(userID: userID) - .navigationTitle(model.serverTitle) - .navigationSubtitle(user?.name ?? "Private Message") - .navigationSplitViewColumnWidth(min: 250, ideal: 500) - .onAppear { - model.markInstantMessagesAsRead(userID: userID) - } - } + switch state.selection { + case .chat: + ChatView() + .navigationTitle(model.serverTitle) + .navigationSubtitle("Public Chat") + .navigationSplitViewColumnWidth(min: 250, ideal: 500) + case .news: + NewsView() + .navigationTitle(model.serverTitle) + .navigationSubtitle("Newsgroups") + .navigationSplitViewColumnWidth(min: 250, ideal: 500) + case .board: + MessageBoardView() + .navigationTitle(model.serverTitle) + .navigationSubtitle("Message Board") + .navigationSplitViewColumnWidth(min: 250, ideal: 500) + case .files: + FilesView() + .navigationTitle(model.serverTitle) + .navigationSubtitle("Shared Files") + .navigationSplitViewColumnWidth(min: 250, ideal: 500) + case .accounts: + AccountManagerView() + .navigationTitle(model.serverTitle) + .navigationSubtitle("Accounts") + .navigationSplitViewColumnWidth(min: 250, ideal: 500) + case .user(let userID): + let user = model.users.first(where: { $0.id == userID }) + MessageView(userID: userID) + .navigationTitle(model.serverTitle) + .navigationSubtitle(user?.name ?? "Private Message") + .navigationSplitViewColumnWidth(min: 250, ideal: 500) + .onAppear { + model.markInstantMessagesAsRead(userID: userID) + } + } } .toolbar(removing: .sidebarToggle) } - - + // MARK: - - + @MainActor func connectToServer() { guard !server.address.isEmpty else { return } - model.login(server: server, username: Prefs.shared.username, iconID: Prefs.shared.userIconID) { success in + model.login(server: server, username: Prefs.shared.username, iconID: Prefs.shared.userIconID) { + success in if !success { print("FAILED LOGIN??") model.disconnect() - } - else { + } else { sendPreferences() model.getUserList() model.downloadBanner() } } } - + private func connectionStatusToProgress(status: HotlineClientStatus) -> Double { switch status { case .disconnected: @@ -563,7 +565,7 @@ struct ServerView: View { return 1.0 } } - + private func connectionStatusToLabel(status: HotlineClientStatus) -> String { let n = server.name ?? server.address switch status { @@ -579,26 +581,28 @@ struct ServerView: View { return "Logged in to \(n)" } } - + @MainActor func sendPreferences() { if self.model.status == .loggedIn { var options: HotlineUserOptions = HotlineUserOptions() - + if Prefs.shared.refusePrivateMessages { options.update(with: .refusePrivateMessages) } - + if Prefs.shared.refusePrivateChat { options.update(with: .refusePrivateChat) } - + if Prefs.shared.enableAutomaticMessage { options.update(with: .automaticResponse) } - + print("Updating preferences with server") - - self.model.sendUserInfo(username: Prefs.shared.username, iconID: Prefs.shared.userIconID, options: options, autoresponse: Prefs.shared.automaticMessage) + + self.model.sendUserInfo( + username: Prefs.shared.username, iconID: Prefs.shared.userIconID, options: options, + autoresponse: Prefs.shared.automaticMessage) } } } @@ -609,30 +613,28 @@ struct ServerView: View { struct TransferItemView: View { let transfer: TransferInfo - + @Environment(\.controlActiveState) private var controlActiveState @Environment(Hotline.self) private var model: Hotline @State private var hovered: Bool = false @State private var buttonHovered: Bool = false - + private func formattedProgressHelp() -> String { if self.transfer.completed { return "File transfer complete" - } - else if self.transfer.failed { + } else if self.transfer.failed { return "File transfer failed" - } - else if self.transfer.progress > 0.0 { + } else if self.transfer.progress > 0.0 { if self.transfer.timeRemaining > 0.0 { - return "\(round(self.transfer.progress * 100.0))% – \(self.transfer.timeRemaining) seconds left" - } - else { + return + "\(round(self.transfer.progress * 100.0))% – \(self.transfer.timeRemaining) seconds left" + } else { return "\(round(self.transfer.progress * 100.0))% complete" } } return "" } - + var body: some View { HStack(alignment: .center, spacing: 5) { HStack(spacing: 0) { @@ -640,17 +642,17 @@ struct TransferItemView: View { FileIconView(filename: transfer.title, fileType: nil) .frame(width: 16, height: 16) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) -// .padding(.leading, 2) + // .padding(.leading, 2) Spacer() } .frame(width: 20) - + Text(transfer.title) .lineLimit(1) .truncationMode(.middle) - + Spacer() - + if self.hovered { Button { model.deleteTransfer(id: transfer.id) @@ -669,16 +671,14 @@ struct TransferItemView: View { .onHover { hovered in self.buttonHovered = hovered } - } - else if transfer.failed { + } else if transfer.failed { Image(systemName: "exclamationmark.triangle.fill") .resizable() .symbolRenderingMode(.multicolor) .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) - } - else if transfer.completed { + } else if transfer.completed { Image(systemName: "checkmark.circle.fill") .resizable() .symbolRenderingMode(.palette) @@ -686,13 +686,11 @@ struct TransferItemView: View { .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) - } - else if transfer.progress == 0.0 { + } else if transfer.progress == 0.0 { ProgressView() .progressViewStyle(.circular) .controlSize(.small) - } - else { + } else { ProgressView(value: transfer.progress, total: 1.0) .progressViewStyle(.circular) .controlSize(.small) diff --git a/Hotline/macOS/SettingsView.swift b/Hotline/macOS/SettingsView.swift index 81a2f8d..c67ba85 100644 --- a/Hotline/macOS/SettingsView.swift +++ b/Hotline/macOS/SettingsView.swift @@ -3,12 +3,12 @@ import SwiftUI struct GeneralSettingsView: View { @State private var username: String = "" @State private var usernameChanged: 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) @@ -49,22 +49,24 @@ struct GeneralSettingsView: View { 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) { + 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)") @@ -77,7 +79,12 @@ struct IconSettingsView: View { .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))) + .background( + iconID == preferences.userIconID + ? Color.accentColor + : (iconID == hoveredUserIconID + ? Color.accentColor.opacity(0.1) : Color(nsColor: .textBackgroundColor)) + ) .clipShape(RoundedRectangle(cornerRadius: 5)) .onTapGesture { preferences.userIconID = iconID @@ -93,7 +100,9 @@ struct IconSettingsView: View { } .background(Color(nsColor: .textBackgroundColor)) .clipShape(RoundedRectangle(cornerRadius: 6)) - .overlay(RoundedRectangle(cornerRadius: 6).stroke(Color(nsColor: .separatorColor), lineWidth: 1)) + .overlay( + RoundedRectangle(cornerRadius: 6).stroke(Color(nsColor: .separatorColor), lineWidth: 1) + ) .onAppear { scrollProxy.scrollTo(preferences.userIconID, anchor: .center) } @@ -107,11 +116,11 @@ struct IconSettingsView: View { 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) @@ -147,7 +156,7 @@ struct SettingsView: View { private enum Tabs: Hashable { case general, icon } - + var body: some View { TabView { GeneralSettingsView() diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift index 1382200..0943d63 100644 --- a/Hotline/macOS/TrackerView.swift +++ b/Hotline/macOS/TrackerView.swift @@ -1,6 +1,6 @@ -import SwiftUI -import SwiftData import Foundation +import SwiftData +import SwiftUI import UniformTypeIdentifiers struct TrackerView: View { @@ -8,7 +8,7 @@ struct TrackerView: View { @Environment(\.openWindow) private var openWindow @Environment(\.controlActiveState) private var controlActiveState @Environment(\.modelContext) private var modelContext - + @State private var refreshing = false @State private var trackerSheetPresented: Bool = false @State private var trackerSheetBookmark: Bookmark? = nil @@ -16,7 +16,7 @@ struct TrackerView: View { @State private var fileDropActive = false @State private var bookmarkExportActive = false @State private var bookmarkExport: BookmarkDocument? = nil - + @Query(sort: \Bookmark.order) private var bookmarks: [Bookmark] @Binding var selection: Bookmark? @@ -32,7 +32,7 @@ struct TrackerView: View { } } .tag(bookmark) - + if bookmark.type == .tracker && bookmark.expanded { ForEach(bookmark.servers, id: \.self) { trackedServer in TrackerItemView(bookmark: trackedServer) @@ -51,7 +51,8 @@ struct TrackerView: View { } .onDeleteCommand { if let bookmark = selection, - bookmark.type != .temporary { + bookmark.type != .temporary + { Bookmark.delete(bookmark, context: modelContext) } } @@ -63,40 +64,42 @@ struct TrackerView: View { print("Tracker: Already attempted to prepopulate bookmarks") return } - + print("Tracker: Prepopulating bookmarks") - + attemptedPrepopulate = true - + // Make sure default bookmarks are there when empty. Bookmark.populateDefaults(context: modelContext) } .onAppear { -// Bookmark.deleteAll(context: modelContext) + // Bookmark.deleteAll(context: modelContext) } .contextMenu(forSelectionType: Bookmark.self) { items in if let item = items.first { if item.type == .temporary { Button { - let newBookmark = Bookmark(type: .server, name: item.name, address: item.address, port: item.port, login: item.login, password: item.password) + let newBookmark = Bookmark( + type: .server, name: item.name, address: item.address, port: item.port, + login: item.login, password: item.password) Bookmark.add(newBookmark, context: modelContext) } label: { Label("Bookmark", systemImage: "bookmark") } - + Divider() } - + Button { NSPasteboard.general.clearContents() NSPasteboard.general.setString(item.displayAddress, forType: .string) } label: { Label("Copy Address", systemImage: "doc.on.doc") } - + if item.type == .tracker || item.type == .server { Divider() - + if item.type == .tracker { Button { trackerSheetBookmark = item @@ -104,7 +107,7 @@ struct TrackerView: View { Label("Edit Tracker...", systemImage: "pencil") } } - + if item.type == .server { Button { bookmarkExport = BookmarkDocument(bookmark: item) @@ -113,13 +116,14 @@ struct TrackerView: View { Label("Export Bookmark...", systemImage: "bookmark.square") } } - + Divider() - + Button { Bookmark.delete(item, context: modelContext) } label: { - Label(item.type == .tracker ? "Delete Tracker" : "Delete Bookmark", systemImage: "trash") + Label( + item.type == .tracker ? "Delete Tracker" : "Delete Bookmark", systemImage: "trash") } } } @@ -127,43 +131,45 @@ struct TrackerView: View { guard let clickedItem = items.first else { return } - + if clickedItem.type == .tracker { if NSEvent.modifierFlags.contains(.option) { trackerSheetBookmark = clickedItem - } - else { + } else { clickedItem.expanded.toggle() } - } - else if let server = clickedItem.server { + } 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 { - case .success(let fileURL): - print("Hotline Bookmark: Successfully exported:", fileURL) - case .failure(let err): - print("Hotline Bookmark: Failed to export:", err) - } - - bookmarkExport = nil - bookmarkExportActive = false - }, onCancellation: {}) + .fileExporter( + isPresented: $bookmarkExportActive, document: bookmarkExport, contentTypes: [.data], + defaultFilename: "\(bookmarkExport?.bookmark.name ?? "Hotline Bookmark").hlbm", + onCompletion: { result in + switch result { + case .success(let fileURL): + print("Hotline Bookmark: Successfully exported:", fileURL) + case .failure(let err): + print("Hotline Bookmark: Failed to export:", err) + } + + bookmarkExport = nil + bookmarkExportActive = false + }, onCancellation: {} + ) .onKeyPress(.rightArrow) { - if - let bookmark = selection, - bookmark.type == .tracker { + if let bookmark = selection, + bookmark.type == .tracker + { bookmark.expanded = true return .handled } return .ignored } .onKeyPress(.leftArrow) { - if - let bookmark = selection, - bookmark.type == .tracker { + if let bookmark = selection, + bookmark.type == .tracker + { bookmark.expanded = false return .handled } @@ -174,26 +180,26 @@ struct TrackerView: View { let _ = provider.loadDataRepresentation(for: UTType.fileURL) { dataRepresentation, err in // HOTLINE CREATOR CODE: 1213484099 // HOTLINE BOOKMARK TYPE CODE: 1213489773 - + if let filePathData = dataRepresentation, - let filePath = String(data: filePathData, encoding: .utf8), - let fileURL = URL(string: filePath) { - + let filePath = String(data: filePathData, encoding: .utf8), + let fileURL = URL(string: filePath) + { + print("Hotline Bookmark: Dropped from ", fileURL.path(percentEncoded: false)) - + DispatchQueue.main.async { if let newBookmark = Bookmark(fileURL: fileURL) { print("Hotline Bookmark: Added bookmark.") Bookmark.add(newBookmark, context: modelContext) - } - else { + } else { print("Hotline Bookmark: Failed to parse.") } } } } } - + return true } .sheet(item: $trackerSheetBookmark) { item in @@ -213,7 +219,7 @@ struct TrackerView: View { .frame(width: 9) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) } - + ToolbarItem(placement: .primaryAction) { Button { refreshing = true @@ -225,7 +231,7 @@ struct TrackerView: View { .disabled(refreshing) .help("Refresh Trackers") } - + ToolbarItem(placement: .primaryAction) { Button { trackerSheetPresented = true @@ -234,7 +240,7 @@ struct TrackerView: View { } .help("Add Tracker") } - + ToolbarItem(placement: .primaryAction) { Button { openWindow(id: "server") @@ -250,30 +256,28 @@ struct TrackerView: View { } }) } - + func refresh() { // When a tracker is selected, refresh only that tracker. - if - let selectedBookmark = selection, - selectedBookmark.type == .tracker { + if let selectedBookmark = selection, + selectedBookmark.type == .tracker + { if !selectedBookmark.expanded { selectedBookmark.expanded = true - } - else { + } else { Task { await selectedBookmark.fetchServers() } } return } - + // Otherwise refresh/expand all trackers. for bookmark in self.bookmarks { if bookmark.type == .tracker { if !bookmark.expanded { bookmark.expanded = true - } - else { + } else { Task { await bookmark.fetchServers() } @@ -286,21 +290,21 @@ struct TrackerView: View { struct TrackerBookmarkSheet: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) private var modelContext - + @State private var bookmark: Bookmark? = nil @State private var trackerAddress: String = "" @State private var trackerName: String = "" - + init() { - + } - + init(_ editingBookmark: Bookmark) { _bookmark = .init(initialValue: editingBookmark) _trackerAddress = .init(initialValue: editingBookmark.displayAddress) _trackerName = .init(initialValue: editingBookmark.name) } - + var body: some View { VStack(alignment: .leading) { Text("Type the address and name of a Hotline Tracker:") @@ -327,11 +331,11 @@ struct TrackerBookmarkSheet: View { Button(self.bookmark != nil ? "Save Tracker" : "Add Tracker") { var displayName = trackerName.trimmingCharacters(in: .whitespacesAndNewlines) let (host, port) = Tracker.parseTrackerAddressAndPort(trackerAddress) - + if displayName.isEmpty { displayName = host } - + if !displayName.isEmpty && !host.isEmpty { if !host.isEmpty { if self.bookmark != nil { @@ -339,16 +343,16 @@ struct TrackerBookmarkSheet: View { self.bookmark?.name = displayName self.bookmark?.address = host self.bookmark?.port = port - } - else { + } else { // We're creating a new bookmark. - let newBookmark = Bookmark(type: .tracker, name: displayName, address: host, port: port) + let newBookmark = Bookmark( + type: .tracker, name: displayName, address: host, port: port) Bookmark.add(newBookmark, context: modelContext) } - + self.trackerName = "" self.trackerAddress = "" - + dismiss() } } @@ -358,7 +362,7 @@ struct TrackerBookmarkSheet: View { Button("Cancel") { self.trackerName = "" self.trackerAddress = "" - + dismiss() } } @@ -368,9 +372,9 @@ struct TrackerBookmarkSheet: View { struct TrackerItemView: View { let bookmark: Bookmark - + @State private var onlineAnimationMaxState: Bool = true - + var body: some View { HStack(alignment: .center, spacing: 6) { if bookmark.type == .tracker { @@ -388,7 +392,7 @@ struct TrackerItemView: View { .padding(.leading, 4) .padding(.trailing, 2) } - + switch bookmark.type { case .tracker: Image("Tracker") @@ -445,11 +449,12 @@ struct TrackerItemView: View { } Spacer(minLength: 0) if let serverUserCount = bookmark.serverUserCount, - serverUserCount > 0 { + serverUserCount > 0 + { Text(String(serverUserCount)) .foregroundStyle(.secondary) .lineLimit(1) - + Circle() .fill(.fileComplete) .frame(width: 7, height: 7) @@ -468,7 +473,7 @@ struct TrackerItemView: View { guard bookmark.type == .tracker else { return } - + if bookmark.expanded { Task { await bookmark.fetchServers() @@ -479,15 +484,15 @@ struct TrackerItemView: View { } #if DEBUG -private struct TrackerViewPreview: View { - @State var selection: Bookmark? = nil + private struct TrackerViewPreview: View { + @State var selection: Bookmark? = nil - var body: some View { - TrackerView(selection: $selection) + var body: some View { + TrackerView(selection: $selection) + } } -} -#Preview { - TrackerViewPreview() -} + #Preview { + TrackerViewPreview() + } #endif |