aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline/macOS')
-rw-r--r--Hotline/macOS/ConnectView.swift147
-rw-r--r--Hotline/macOS/Files/FilePreviewQuickLookView.swift22
-rw-r--r--Hotline/macOS/HotlinePanelView.swift2
-rw-r--r--Hotline/macOS/ServerView.swift261
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