aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS/ServerView.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline/macOS/ServerView.swift')
-rw-r--r--Hotline/macOS/ServerView.swift346
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)