diff options
Diffstat (limited to 'Hotline/macOS')
| -rw-r--r-- | Hotline/macOS/ConnectView.swift | 147 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FilePreviewQuickLookView.swift | 22 | ||||
| -rw-r--r-- | Hotline/macOS/HotlinePanelView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 261 |
4 files changed, 215 insertions, 217 deletions
diff --git a/Hotline/macOS/ConnectView.swift b/Hotline/macOS/ConnectView.swift new file mode 100644 index 0000000..8419522 --- /dev/null +++ b/Hotline/macOS/ConnectView.swift @@ -0,0 +1,147 @@ +import SwiftUI + +struct ConnectView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var modelContext + + @Binding var address: String + @Binding var login: String + @Binding var password: String + + var action: (() -> Void)? = nil + + @State private var bookmarkSheetPresented: Bool = false + @State private var bookmarkName: String = "" + + private enum FocusFields { + case address + case login + case password + } + + @FocusState private var focusedField: FocusFields? + + var body: some View { + VStack(alignment: .center, spacing: 0) { + Form { + HStack(alignment: .top, spacing: 10) { + Image("Server Large") + .resizable() + .scaledToFit() + .frame(width: 28, height: 28) + + VStack(alignment: .leading) { + Text("Connect to Server") + Text("Enter the address of a Hotline server to connect to.") + .foregroundStyle(.secondary) + .font(.subheadline) + } + } + + TextField(text: self.$address) { + Text("Address") + } + .focused($focusedField, equals: .address) + + TextField(text: self.$login, prompt: Text("Optional")) { + Text("Login") + } + .focused($focusedField, equals: .login) + + SecureField(text: self.$password, prompt: Text("Optional")) { + Text("Password") + } + .focused($focusedField, equals: .password) + } + .formStyle(.grouped) + .fixedSize(horizontal: false, vertical: true) + + HStack { + Button { + if !self.address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + self.bookmarkSheetPresented = true + } + } label: { + Image(systemName: "bookmark.fill") + } + .disabled(self.address.isEmpty) + .controlSize(.regular) + .buttonStyle(.automatic) + .help("Bookmark server") + + Spacer() + + Button("Cancel") { + self.dismiss() + } + .controlSize(.regular) + .buttonStyle(.automatic) + .keyboardShortcut(.cancelAction) + + Button("Connect") { + self.action?() +// self.connectToServer() + } + .controlSize(.regular) + .buttonStyle(.automatic) + .keyboardShortcut(.defaultAction) + } + .padding(.horizontal, 20) + } +// .onChange(of: self.address) { +// let (a, p) = Server.parseServerAddressAndPort(connectAddress) +// server.address = a +// server.port = p +// } +// .onChange(of: connectLogin) { +// server.login = connectLogin.trimmingCharacters(in: .whitespacesAndNewlines) +// } +// .onChange(of: connectPassword) { +// server.password = connectPassword +// } + .frame(maxWidth: 380) + .padding() + .onAppear { + self.focusedField = .address + } + .sheet(isPresented: self.$bookmarkSheetPresented) { + VStack(alignment: .leading) { + Text("Save Bookmark") + .foregroundStyle(.secondary) + .padding(.bottom, 4) + TextField("Bookmark Name", text: self.$bookmarkName) + .textFieldStyle(.roundedBorder) + .controlSize(.large) + } + .frame(width: 250) + .padding() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + self.bookmarkSheetPresented = false + self.bookmarkName = "" + } + } + + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + let name = String(self.bookmarkName.trimmingCharacters(in: .whitespacesAndNewlines)) + if !name.isEmpty { + self.bookmarkSheetPresented = false + self.bookmarkName = "" + + let (host, port) = Server.parseServerAddressAndPort(self.address) + let login: String? = self.login.isBlank ? nil : self.login + let password: String? = self.password.isBlank ? nil : self.password + + if !host.isEmpty { + let newBookmark = Bookmark(type: .server, name: name, address: host, port: port, login: login, password: password) + Bookmark.add(newBookmark, context: modelContext) + } + } + } + } + } + } + } +} diff --git a/Hotline/macOS/Files/FilePreviewQuickLookView.swift b/Hotline/macOS/Files/FilePreviewQuickLookView.swift index 0323fdd..26bd286 100644 --- a/Hotline/macOS/Files/FilePreviewQuickLookView.swift +++ b/Hotline/macOS/Files/FilePreviewQuickLookView.swift @@ -1,10 +1,3 @@ -// -// FilePreviewQuickLookView.swift -// Hotline -// -// QuickLook-based file preview window for all supported file types -// - import SwiftUI import UniformTypeIdentifiers @@ -15,10 +8,11 @@ struct FilePreviewQuickLookView: View { @Environment(\.controlActiveState) private var controlActiveState @Environment(\.colorScheme) private var colorScheme - @Environment(\.dismiss) var dismiss + @Environment(\.dismiss) private var dismiss @Binding var info: PreviewFileInfo? @State var preview: FilePreviewState? = nil + @FocusState private var focusField: FilePreviewFocus? var body: some View { @@ -76,14 +70,14 @@ struct FilePreviewQuickLookView: View { .background(Color(nsColor: .textBackgroundColor)) .focused($focusField, equals: .window) .navigationTitle(info?.name ?? "File Preview") - .background( + .background { + if let fileURL = self.preview?.fileURL { WindowConfigurator { window in - if let fileURL = preview?.fileURL { - window.representedURL = fileURL - window.standardWindowButton(.documentIconButton)?.isHidden = false - } + window.representedURL = fileURL + window.standardWindowButton(.documentIconButton)?.isHidden = false } - ) + } + } .toolbar { if let _ = preview?.fileURL { if let info = info { diff --git a/Hotline/macOS/HotlinePanelView.swift b/Hotline/macOS/HotlinePanelView.swift index 220ea97..a56cd44 100644 --- a/Hotline/macOS/HotlinePanelView.swift +++ b/Hotline/macOS/HotlinePanelView.swift @@ -124,7 +124,7 @@ struct HotlinePanelView: View { .buttonStyle(.plain) .frame(width: 20, height: 20) .disabled(self.activeServerState == nil) - .help("Accounts") + .help("Administration") } Button { diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift index 44274ea..757dbd8 100644 --- a/Hotline/macOS/ServerView.swift +++ b/Hotline/macOS/ServerView.swift @@ -47,10 +47,8 @@ struct ListItemView: View { if let i = icon { Image(i) .resizable() - // .renderingMode(.template) .scaledToFit() .frame(width: 20, height: 20) -// .padding(.leading, 2) .opacity(controlActiveState == .inactive ? 0.5 : 1.0) } @@ -74,29 +72,28 @@ extension FocusedValues { } struct ServerView: View { - @Environment(\.dismiss) var dismiss + @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme @Environment(\.controlActiveState) private var controlActiveState @Environment(\.scenePhase) private var scenePhase @Environment(\.modelContext) private var modelContext + @Binding var server: Server + @State private var model: HotlineState = HotlineState() @State private var state: ServerState = ServerState(selection: .chat) @State private var agreementShown: Bool = false @State private var connectAddress: String = "" @State private var connectLogin: String = "" @State private var connectPassword: String = "" - @State private var connectNameSheetPresented: Bool = false - @State private var connectName: String = "" - - @Binding var server: Server + @State private var connectionDisplayed: Bool = false static var menuItems: [ServerMenuItem] = [ ServerMenuItem(type: .chat, name: "Chat", image: "Section Chat"), ServerMenuItem(type: .board, name: "Board", image: "Section Board"), ServerMenuItem(type: .news, name: "News", image: "Section News"), ServerMenuItem(type: .files, name: "Files", image: "Section Files"), - ServerMenuItem(type: .accounts, name: "Accounts", image: "Section Users"), + ServerMenuItem(type: .accounts, name: "Admin", image: "Section Users"), ] static var classicMenuItems: [ServerMenuItem] = [ @@ -105,50 +102,20 @@ struct ServerView: View { 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 { + if self.model.status == .disconnected { VStack(alignment: .center) { Spacer() - self.connectForm +// if self.connectionDisplayed { + self.connectForm +// } Spacer() } .navigationTitle("Connect to Server") - .onAppear { - self.focusedField = .address - } - } - else if case .failed(let error) = model.status { - VStack { - Image("Hotline") - .resizable() - .renderingMode(.template) - .scaledToFit() - .foregroundColor(Color(hex: 0xE10000)) - .frame(width: 18) - .opacity(controlActiveState == .inactive ? 0.5 : 1.0) - .padding(.trailing, 4) - - Text("Connection Failed") - .font(.headline) - Text(error) - .font(.caption) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - } - .frame(maxWidth: 300) - .padding() - .navigationTitle("Connection Failed") +// .animation(.default.delay(0.25), value: self.connectionDisplayed) } - else if model.status != .loggedIn { + else if self.model.status.isLoggingIn { HStack { Image("Hotline") .resizable() @@ -156,38 +123,38 @@ struct ServerView: View { .scaledToFit() .foregroundColor(Color(hex: 0xE10000)) .frame(width: 18) - .opacity(controlActiveState == .inactive ? 0.5 : 1.0) + .opacity(self.controlActiveState == .inactive ? 0.5 : 1.0) .padding(.trailing, 4) - ProgressView(value: connectionStatusToProgress(status: model.status)) { - Text(connectionStatusToLabel(status: model.status)) + ProgressView(value: connectionStatusToProgress(status: self.model.status)) { + Text(connectionStatusToLabel(status: self.model.status)) } - .accentColor(colorScheme == .dark ? .white : .black) + .accentColor(self.colorScheme == .dark ? .white : .black) } .frame(maxWidth: 300) .padding() .navigationTitle("Connecting to Server") } - else { - serverView - .environment(model) + else if self.model.status == .loggedIn { + self.serverView + .environment(self.model) .onChange(of: Prefs.shared.userIconID) { - Task { try? await model.sendUserPreferences() } + Task { try? await self.model.sendUserPreferences() } } .onChange(of: Prefs.shared.username) { - Task { try? await model.sendUserPreferences() } + Task { try? await self.model.sendUserPreferences() } } .onChange(of: Prefs.shared.refusePrivateMessages) { - Task { try? await model.sendUserPreferences() } + Task { try? await self.model.sendUserPreferences() } } .onChange(of: Prefs.shared.refusePrivateChat) { - Task { try? await model.sendUserPreferences() } + Task { try? await self.model.sendUserPreferences() } } .onChange(of: Prefs.shared.enableAutomaticMessage) { - Task { try? await model.sendUserPreferences() } + Task { try? await self.model.sendUserPreferences() } } .onChange(of: Prefs.shared.automaticMessage) { - Task { try? await model.sendUserPreferences() } + Task { try? await self.model.sendUserPreferences() } } .toolbar { if #available(macOS 26.0, *) { @@ -196,7 +163,7 @@ struct ServerView: View { .resizable() .scaledToFit() .frame(width: 28) - .opacity(controlActiveState == .inactive ? 0.4 : 1.0) + .opacity(self.controlActiveState == .inactive ? 0.4 : 1.0) } .sharedBackgroundVisibility(.hidden) } @@ -206,7 +173,7 @@ struct ServerView: View { .resizable() .scaledToFit() .frame(width: 28) - .opacity(controlActiveState == .inactive ? 0.4 : 1.0) + .opacity(self.controlActiveState == .inactive ? 0.4 : 1.0) } } } @@ -214,164 +181,54 @@ struct ServerView: View { } .onDisappear { Task { - await model.disconnect() + await self.model.disconnect() } } - .onChange(of: model.serverTitle) { - state.serverName = model.serverTitle + .onChange(of: self.model.serverTitle) { + self.state.serverName = self.model.serverTitle } -// .onChange(of: model.bannerImage) { -// state.serverBanner = model.bannerImage -// } -// .onChange(of: model.bannerColors) { -// guard let backgroundColor = model.bannerColors?.backgroundColor else { -// state.bannerBackgroundColor = nil -// return -// } -// state.bannerBackgroundColor = Color(nsColor: backgroundColor) -// } - .alert(model.errorMessage ?? "Server Error", isPresented: $model.errorDisplayed) { + .alert(self.model.errorMessage ?? "Server Error", isPresented: self.$model.errorDisplayed) { Button("OK") {} } - .task { - var address = server.address - if server.port != HotlinePorts.DefaultServerPort { - address += ":\(server.port)" + .onAppear { + var address = self.server.address + if self.server.port != HotlinePorts.DefaultServerPort { + address += ":\(self.server.port)" } - connectAddress = server.address - connectLogin = server.login - connectPassword = server.password + self.connectAddress = self.server.address + self.connectLogin = self.server.login + self.connectPassword = self.server.password // Connect to server automatically unless the option key is held down. if !NSEvent.modifierFlags.contains(.option) { - connectToServer() + self.connectToServer() } + + self.connectionDisplayed = true } .focusedSceneValue(\.activeHotlineModel, model) .focusedSceneValue(\.activeServerState, state) } - var connectForm: some View { - VStack(alignment: .center, spacing: 0) { - Form { - HStack(alignment: .top, spacing: 10) { - Image("Server Large") - .resizable() - .scaledToFit() - .frame(width: 28, height: 28) - - VStack(alignment: .leading) { - Text("Connect to Server") - Text("Enter the address of a Hotline server to connect to.") - .foregroundStyle(.secondary) - .font(.subheadline) - } - } - - TextField(text: $connectAddress) { - Text("Address") - } - .focused($focusedField, equals: .address) - - TextField(text: $connectLogin, prompt: Text("Optional")) { - Text("Login") - } - .focused($focusedField, equals: .login) - - SecureField(text: $connectPassword, prompt: Text("Optional")) { - Text("Password") - } - .focused($focusedField, equals: .password) - } - .formStyle(.grouped) - .fixedSize(horizontal: false, vertical: true) - - HStack { - Button { - if !connectAddress.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - connectNameSheetPresented = true - } - } label: { - Image(systemName: "bookmark.fill") - } - .disabled(connectAddress.isEmpty) - .controlSize(.regular) - .buttonStyle(.automatic) - .help("Bookmark server") - - Spacer() - - Button("Cancel") { - dismiss() - } - .controlSize(.regular) - .buttonStyle(.automatic) - .keyboardShortcut(.cancelAction) - - Button("Connect") { - connectToServer() - } - .controlSize(.regular) - .buttonStyle(.automatic) - .keyboardShortcut(.defaultAction) - } - .padding(.horizontal, 20) + private var connectForm: some View { + ConnectView(address: self.$connectAddress, login: self.$connectLogin, password: self.$connectPassword) { + self.connectToServer() } - .onChange(of: connectAddress) { - let (a, p) = Server.parseServerAddressAndPort(connectAddress) - server.address = a - server.port = p + .focusSection() + .onChange(of: self.connectAddress) { + let (a, p) = Server.parseServerAddressAndPort(self.connectAddress) + self.server.address = a + self.server.port = p } - .onChange(of: connectLogin) { - server.login = connectLogin.trimmingCharacters(in: .whitespacesAndNewlines) + .onChange(of: self.connectLogin) { + self.server.login = self.connectLogin.trimmingCharacters(in: .whitespacesAndNewlines) } - .onChange(of: connectPassword) { - server.password = connectPassword - } - .frame(maxWidth: 380) - .padding() - .sheet(isPresented: $connectNameSheetPresented) { - VStack(alignment: .leading) { - Text("Save Bookmark") - .foregroundStyle(.secondary) - .padding(.bottom, 4) - TextField("Bookmark Name", text: $connectName) - .textFieldStyle(.roundedBorder) - .controlSize(.large) - } - .frame(width: 250) - .padding() - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - connectNameSheetPresented = false - connectName = "" - } - } - - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - let name = String(connectName.trimmingCharacters(in: .whitespacesAndNewlines)) - if !name.isEmpty { - connectNameSheetPresented = false - connectName = "" - - let (host, port) = Server.parseServerAddressAndPort(connectAddress) - let login: String? = connectLogin.isEmpty ? nil : connectLogin - let password: String? = connectPassword.isEmpty ? nil : connectPassword - - if !host.isEmpty { - let newBookmark = Bookmark(type: .server, name: name, address: host, port: port, login: login, password: password) - Bookmark.add(newBookmark, context: modelContext) - } - } - } - } - } + .onChange(of: self.connectPassword) { + self.server.password = self.connectPassword } } - var navigationList: some View { + private 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 @@ -496,7 +353,7 @@ struct ServerView: View { case .accounts: AccountManagerView() .navigationTitle(model.serverTitle) - .navigationSubtitle("Accounts") + .navigationSubtitle("Administration") .navigationSplitViewColumnWidth(min: 250, ideal: 500) case .user(let userID): let user = model.users.first(where: { $0.id == userID }) @@ -510,21 +367,21 @@ struct ServerView: View { } } .toolbar(removing: .sidebarToggle) - } - // MARK: - @MainActor func connectToServer() { - guard !server.address.isEmpty else { + guard !self.server.address.isEmpty else { return } + + // Set status here so it's immediate (not waiting to enter task). + self.model.status = .connecting Task { @MainActor in do { - // login() handles everything: connect, getUserList, sendPreferences, downloadBanner - try await model.login( + try await self.model.login( server: server, username: Prefs.shared.username, iconID: Prefs.shared.userIconID |