diff options
Diffstat (limited to 'Hotline/macOS/ServerView.swift')
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 346 |
1 files changed, 172 insertions, 174 deletions
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) |