aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-02-05 22:27:17 +0100
committerRuben Beltran del Rio <git@r.bdr.sh>2025-02-05 22:27:17 +0100
commit45829daa856b376b1ab04d415917110b71b1dec5 (patch)
tree8e52daed6897b6f489d455736fe256cb9bd90fef /Hotline/macOS
parent5c3ea897d062a47bc8cd6255fb8c36bad2f0733f (diff)
Apply formatting
Diffstat (limited to 'Hotline/macOS')
-rw-r--r--Hotline/macOS/AboutView.swift114
-rw-r--r--Hotline/macOS/AccountManagerView.swift117
-rw-r--r--Hotline/macOS/ChatView.swift161
-rw-r--r--Hotline/macOS/FileDetailsView.swift62
-rw-r--r--Hotline/macOS/FilePreviewImageView.swift24
-rw-r--r--Hotline/macOS/FilePreviewTextView.swift22
-rw-r--r--Hotline/macOS/FilesView.swift182
-rw-r--r--Hotline/macOS/HotlinePanelView.swift110
-rw-r--r--Hotline/macOS/MessageBoardEditorView.swift60
-rw-r--r--Hotline/macOS/MessageBoardView.swift63
-rw-r--r--Hotline/macOS/MessageView.swift34
-rw-r--r--Hotline/macOS/NewsEditorView.swift52
-rw-r--r--Hotline/macOS/NewsItemView.swift97
-rw-r--r--Hotline/macOS/NewsView.swift96
-rw-r--r--Hotline/macOS/ServerAgreementView.swift60
-rw-r--r--Hotline/macOS/ServerMessageView.swift12
-rw-r--r--Hotline/macOS/ServerView.swift346
-rw-r--r--Hotline/macOS/SettingsView.swift47
-rw-r--r--Hotline/macOS/TrackerView.swift181
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) ![image 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