aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS/TrackerView.swift
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2024-01-17 23:18:27 -0800
committerDustin Mierau <dustin@mierau.me>2024-01-17 23:18:27 -0800
commit1b303803e53dbeb24dab1a7c714d71ee14f002db (patch)
tree4616861b23642d26c9b58b5925315232c1fdb975 /Hotline/macOS/TrackerView.swift
parent8e85fbbdadcb993cd270ff4bf60d2280b9e7ad56 (diff)
Added server and tracker bookmarks. Reordering in Servers window. Delete. All stored securely in Keychain.
Diffstat (limited to 'Hotline/macOS/TrackerView.swift')
-rw-r--r--Hotline/macOS/TrackerView.swift568
1 files changed, 327 insertions, 241 deletions
diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift
index 6b8f330..073ecdf 100644
--- a/Hotline/macOS/TrackerView.swift
+++ b/Hotline/macOS/TrackerView.swift
@@ -1,104 +1,274 @@
import SwiftUI
-enum TrackerBookmarkType: String {
- case tracker = "tracker"
- case server = "server"
-}
-
-struct TrackerBookmark {
- let type: TrackerBookmarkType
- let name: String
- let address: String
- let port: Int = HotlinePorts.DefaultServerPort
-}
-
-@Observable
-class TrackerItem: Identifiable, Hashable {
- let id: UUID = UUID()
- let bookmark: TrackerBookmark?
- let server: Server?
-
- var servers: [TrackerItem]?
-
- var expanded: Bool = false
- var loading: Bool = false
+struct TrackerView: View {
+ @Environment(Bookmarks.self) private var bookmarks: Bookmarks
+ @Environment(\.colorScheme) private var colorScheme
+ @Environment(\.openWindow) private var openWindow
+ @Environment(\.controlActiveState) private var controlActiveState
- var displayAddress: String? {
- if let s = server {
- if s.port == HotlinePorts.DefaultServerPort {
- return s.address
+ @MainActor
+ func refresh() async {
+
+ // When a tracker is selected, refresh only that tracker.
+ if
+ let selectedItem = selection,
+ let bookmark = selectedItem.bookmark,
+ bookmark.type == .tracker {
+ if !selectedItem.expanded {
+ selectedItem.expanded = true
}
else {
- return "\(s.address):\(s.port)"
+ await selectedItem.loadServers()
}
+ return
}
- else if let b = bookmark {
- if b.port == HotlinePorts.DefaultServerPort {
- return b.address
- }
- else {
- return "\(b.address):\(b.port)"
+
+ // Otherwise refresh/expand all trackers.
+ for server in self.servers {
+ if
+ let bookmark = server.bookmark,
+ bookmark.type == .tracker {
+ if !server.expanded {
+ server.expanded = true
+ }
+ else {
+ Task {
+ await server.loadServers()
+ }
+ }
}
}
-
- return nil
}
- init(bookmark: TrackerBookmark) {
- self.bookmark = bookmark
- self.server = nil
- self.servers = nil
-
- if bookmark.type == .tracker {
- self.servers = []
- }
- }
+ @State private var servers: [TrackerItem] = []
+ @State private var selection: TrackerItem? = nil
- init(server: Server) {
- self.server = server
- self.servers = nil
- self.bookmark = nil
- }
+ @State private var scrollOffset: CGFloat = CGFloat.zero
+ @State private var initialLoadComplete = false
+ @State private var refreshing = false
+ @State private var topBarOpacity: Double = 1.0
+ @State private var connectVisible = false
+ @State private var connectDismissed = true
+ @State private var serverVisible = false
- static func == (lhs: TrackerItem, rhs: TrackerItem) -> Bool {
- return lhs.id == rhs.id
- }
+ @State private var trackerSheetPresented: Bool = false
+ @State private var trackerName: String = ""
+ @State private var trackerAddress: String = ""
- func hash(into hasher: inout Hasher) {
- hasher.combine(self.id)
+ var trackerBookmarkSheet: some View {
+ VStack(alignment: .leading) {
+ Text("Type the address and name of a Hotline Tracker:")
+ .foregroundStyle(.secondary)
+ .padding(.bottom, 8)
+ Form {
+ Group {
+ TextField(text: $trackerAddress) {
+ Text("Address:")
+ }
+ TextField(text: $trackerName, prompt: Text("Optional")) {
+ Text("Name:")
+ }
+ }
+ .textFieldStyle(.roundedBorder)
+ .controlSize(.large)
+ }
+ }
+ .frame(width: 300)
+ .fixedSize(horizontal: true, vertical: true)
+ .padding()
+ .toolbar {
+ ToolbarItem(placement: .confirmationAction) {
+ Button("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 {
+ let _ = bookmarks.add(Bookmark(type: .tracker, name: displayName, address: host, port: port))
+ trackerSheetPresented = false
+ trackerName = ""
+ trackerAddress = ""
+ }
+ }
+ }
+ }
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Cancel") {
+ trackerSheetPresented = false
+ trackerName = ""
+ trackerAddress = ""
+ }
+ }
+ }
}
-
- @MainActor
- func loadServers() async {
- guard
- let bookmark = self.bookmark,
- bookmark.type == .tracker
- else {
- self.loading = false
- return
+ var body: some View {
+ List($servers, id: \.self, editActions: [.move], selection: $selection) { $item in
+ TrackerItemView(item: item, depth: 0)
+ .deleteDisabled(!item.editable)
+ .moveDisabled(!item.editable)
+ .tag(item)
}
-
- let client = HotlineTrackerClient()
-
- self.loading = true
-
- if let fetchedServers: [HotlineServer] = try? await client.fetchServers(address: bookmark.address, port: HotlinePorts.DefaultTrackerPort) {
- var newItems: [TrackerItem] = []
-
- for s in fetchedServers {
- if let serverName = s.name {
- let server = Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))
- newItems.append(TrackerItem(server: server))
+ .environment(\.defaultMinListRowHeight, 34)
+ .listStyle(.inset)
+ .alternatingRowBackgrounds(.enabled)
+ .onDeleteCommand {
+ if let sel = selection, let bookmark = sel.bookmark {
+ let _ = bookmarks.delete(bookmark)
+ if let i = self.servers.firstIndex(where: { $0.id == sel.id }) {
+ self.servers.remove(at: i)
}
+ selection = nil
+ }
+ }
+ .contextMenu(forSelectionType: TrackerItem.self) { items in
+ if let item = items.first {
+ if let server = item.server {
+ Button {
+ let _ = bookmarks.add(Bookmark(type: .server, name: server.name ?? server.address, address: server.address, port: server.port))
+ } label: {
+ Label("Bookmark", systemImage: "bookmark")
+ }
+ }
+
+ if let address = item.displayAddress {
+ Button {
+ NSPasteboard.general.clearContents()
+ NSPasteboard.general.setString(address, forType: .string)
+ } label: {
+ Label("Copy Address", systemImage: "doc.on.doc")
+ }
+ }
+ }
+ } primaryAction: { items in
+ guard let clickedItem = items.first else {
+ return
+ }
+
+ if
+ let bookmark = clickedItem.bookmark,
+ bookmark.type == .tracker {
+ clickedItem.expanded.toggle()
+ }
+ else if let server = clickedItem.server {
+ openWindow(id: "server", value: server)
+ }
+ else if let bookmark = clickedItem.bookmark, bookmark.type == .server {
+ let server = Server(name: bookmark.name, description: nil, address: bookmark.address, port: HotlinePorts.DefaultServerPort)
+ openWindow(id: "server", value: server)
+ }
+ }
+ .onKeyPress(.rightArrow) {
+ if
+ let selectedItem = selection,
+ let bookmark = selectedItem.bookmark,
+ bookmark.type == .tracker {
+ selectedItem.expanded = true
+ return .handled
+ }
+ return .ignored
+ }
+ .onKeyPress(.leftArrow) {
+ if
+ let selectedItem = selection,
+ let bookmark = selectedItem.bookmark,
+ bookmark.type == .tracker {
+ selectedItem.expanded = false
+ return .handled
+ }
+ return .ignored
+ }
+ .sheet(isPresented: $trackerSheetPresented) {
+ trackerBookmarkSheet
+ }
+ .navigationTitle("Servers")
+ .toolbar {
+ ToolbarItem(placement: .navigation) {
+ Image("Hotline")
+ .resizable()
+ .renderingMode(.template)
+ .scaledToFit()
+ .foregroundColor(Color(hex: 0xE10000))
+ .frame(width: 9)
+ .opacity(controlActiveState == .inactive ? 0.5 : 1.0)
+ }
+ ToolbarItem(placement: .primaryAction) {
+ Button {
+ Task {
+ initialLoadComplete = false
+ await refresh()
+ initialLoadComplete = true
+ }
+ } label: {
+ Label("Refresh", systemImage: "arrow.clockwise")
+ }
+ .help("Refresh Trackers")
+ }
+
+ ToolbarItem(placement: .primaryAction) {
+ Button {
+ trackerSheetPresented = true
+ } label: {
+ Label("Add Tracker", systemImage: "point.3.filled.connected.trianglepath.dotted")
+ }
+ .help("Add Tracker")
+ }
+
+ ToolbarItem(placement: .primaryAction) {
+ Button {
+ openWindow(id: "server")
+ } label: {
+ Label("Connect to Server", systemImage: "globe.americas.fill")
+ }
+ .help("Connect to Server")
+ }
+ }
+ .task {
+ guard let bookmarks = bookmarks.bookmarks else {
+ return
+ }
+ // Add initial items to tracker list.
+ var items: [TrackerItem] = []
+ for bookmark in bookmarks {
+ items.append(TrackerItem(bookmark: bookmark))
+ }
+ self.servers = items
+ }
+ .onChange(of: servers) {
+ Task {
+ saveBookmarks()
+ }
+ }
+ .onReceive(NotificationCenter.default.publisher(for: NSNotification.BookmarkAdded)) { notification in
+ guard let bookmarks = bookmarks.bookmarks, let userInfo = notification.userInfo else {
+ return
+ }
+
+ if let i = userInfo["index"] as? Int, bookmarks.count > i {
+ self.servers.insert(TrackerItem(bookmark: bookmarks[i]), at: i)
+ }
+ }
+ .onOpenURL(perform: { url in
+ if let s = Server(url: url) {
+ openWindow(id: "server", value: s)
+ }
+ })
+ }
+
+ private func saveBookmarks() {
+ var newBookmarks: [Bookmark] = []
+ for server in self.servers {
+ if let b = server.bookmark {
+ newBookmarks.append(b)
}
-
- self.servers = newItems
}
- self.loading = false
+ bookmarks.apply(newBookmarks)
}
-
}
struct TrackerItemView: View {
@@ -217,190 +387,106 @@ struct TrackerItemView: View {
item.expanded,
let servers = item.servers {
ForEach(servers, id: \.self) { serverItem in
- TrackerItemView(item: serverItem, depth: self.depth + 1).tag(serverItem)
+ TrackerItemView(item: serverItem, depth: self.depth + 1)
+ .tag(serverItem)
+ .deleteDisabled(!serverItem.editable)
+ .moveDisabled(!serverItem.editable)
}
}
}
}
-struct TrackerView: View {
- @Environment(\.colorScheme) private var colorScheme
- @Environment(\.openWindow) private var openWindow
- @Environment(\.controlActiveState) private var controlActiveState
+@Observable
+class TrackerItem: Identifiable, Hashable {
+ let id: UUID = UUID()
+ let bookmark: Bookmark?
+ let server: Server?
- var bookmarks: [TrackerBookmark] = [
- TrackerBookmark(type: .server, name: "RetroMac", address: "82.32.130.173"),
- TrackerBookmark(type: .server, name: "System 7 Today", address: "hotline.system7today.com"), //host has dynamic ip
- TrackerBookmark(type: .tracker, name: "Featured Servers", address: "hltracker.com"),
- TrackerBookmark(type: .tracker, name: "Agent79", address: "tracked.agent79.org"),
- TrackerBookmark(type: .tracker, name: "Preterhuman", address: "tracker.preterhuman.net"),
-
- // "hltracker.com"
- // "tracker.preterhuman.net"
- // "hotline.ubersoft.org"
- // "tracked.nailbat.com"
- // "hotline.duckdns.org"
- // "tracked.agent79.org"
-
- ]
+ var editable: Bool = true
- @MainActor
- func refresh() async {
-
- // When a tracker is selected, refresh only that tracker.
- if
- let selectedItem = selection,
- let bookmark = selectedItem.bookmark,
- bookmark.type == .tracker {
- if !selectedItem.expanded {
- selectedItem.expanded.toggle()
+ var servers: [TrackerItem]?
+
+ var expanded: Bool = false
+ var loading: Bool = false
+
+ var displayAddress: String? {
+ if let s = server {
+ if s.port == HotlinePorts.DefaultServerPort {
+ return s.address
}
else {
- await selectedItem.loadServers()
+ return "\(s.address):\(s.port)"
}
- return
}
-
- // Otherwise refresh/expand all trackers.
- for server in self.servers {
- if
- let bookmark = server.bookmark,
- bookmark.type == .tracker {
- if !server.expanded {
- server.expanded.toggle()
- }
- else {
- Task {
- await server.loadServers()
- }
- }
+ else if let b = bookmark {
+ if b.port == HotlinePorts.DefaultServerPort {
+ return b.address
+ }
+ else {
+ return "\(b.address):\(b.port)"
}
}
+
+ return nil
}
- @State private var servers: [TrackerItem] = []
- @State private var selection: TrackerItem? = nil
+ init(bookmark: Bookmark) {
+ self.bookmark = bookmark
+ self.server = nil
+ self.servers = nil
+
+ if bookmark.type == .tracker {
+ self.servers = []
+ }
+ }
- @State private var scrollOffset: CGFloat = CGFloat.zero
- @State private var initialLoadComplete = false
- @State private var refreshing = false
- @State private var topBarOpacity: Double = 1.0
- @State private var connectVisible = false
- @State private var connectDismissed = true
- @State private var serverVisible = false
+ init(server: Server) {
+ self.server = server
+ self.servers = nil
+ self.bookmark = nil
+ }
- var body: some View {
- List(self.servers, id: \.self, selection: $selection) { item in
- TrackerItemView(item: item, depth: 0)
- .tag(item)
- }
- .environment(\.defaultMinListRowHeight, 34)
- .listStyle(.inset)
- .alternatingRowBackgrounds(.enabled)
- .contextMenu(forSelectionType: TrackerItem.self) { items in
- if let item = items.first {
- if let address = item.displayAddress {
- Button {
- NSPasteboard.general.clearContents()
- NSPasteboard.general.setString(address, forType: .string)
- } label: {
- Label("Copy Server Address", systemImage: "doc.on.doc")
- }
- }
- }
- } primaryAction: { items in
- guard let clickedItem = items.first else {
- return
- }
-
- if
- let bookmark = clickedItem.bookmark,
- bookmark.type == .tracker {
- clickedItem.expanded.toggle()
- }
- else if let server = clickedItem.server {
- openWindow(id: "server", value: server)
- }
- else if let bookmark = clickedItem.bookmark, bookmark.type == .server {
- let server = Server(name: bookmark.name, description: nil, address: bookmark.address, port: HotlinePorts.DefaultServerPort)
- openWindow(id: "server", value: server)
- }
- }
- .onKeyPress(.rightArrow) {
- if
- let selectedItem = selection,
- let bookmark = selectedItem.bookmark,
- bookmark.type == .tracker {
- selectedItem.expanded = true
- return .handled
- }
- return .ignored
- }
- .onKeyPress(.leftArrow) {
- if
- let selectedItem = selection,
- let bookmark = selectedItem.bookmark,
- bookmark.type == .tracker {
- selectedItem.expanded = false
- return .handled
- }
- return .ignored
+ static func == (lhs: TrackerItem, rhs: TrackerItem) -> Bool {
+ return lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(self.id)
+ }
+
+
+ @MainActor
+ func loadServers() async {
+ guard
+ let bookmark = self.bookmark,
+ bookmark.type == .tracker
+ else {
+ self.loading = false
+ return
}
- .navigationTitle("Servers")
- .toolbar {
- ToolbarItem(placement: .navigation) {
- Image("Hotline")
- .resizable()
- .renderingMode(.template)
- .scaledToFit()
- .foregroundColor(Color(hex: 0xE10000))
- .frame(width: 9)
- .opacity(controlActiveState == .inactive ? 0.5 : 1.0)
- }
- ToolbarItem(placement: .primaryAction) {
- Button {
- Task {
- initialLoadComplete = false
- await refresh()
- initialLoadComplete = true
- }
- } label: {
- Label("Refresh", systemImage: "arrow.clockwise")
- }
- .help("Refresh Trackers")
- }
-
- ToolbarItem(placement: .primaryAction) {
- Button {
- } label: {
- Label("Add Tracker...", systemImage: "point.3.filled.connected.trianglepath.dotted")
- }
- .help("Add Tracker")
- }
-
- ToolbarItem(placement: .primaryAction) {
- Button {
- openWindow(id: "server")
- } label: {
- Label("Add Server Bookmark...", systemImage: "globe.americas.fill")
+
+ let client = HotlineTrackerClient()
+
+ self.loading = true
+
+ if let fetchedServers: [HotlineServer] = try? await client.fetchServers(address: bookmark.address, port: HotlinePorts.DefaultTrackerPort) {
+ var newItems: [TrackerItem] = []
+
+ for s in fetchedServers {
+ if let serverName = s.name {
+ let server = Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))
+ let item = TrackerItem(server: server)
+ item.editable = false
+ newItems.append(item)
}
- .help("Connect to Server...")
- }
- }
- .task {
- // Add initial items to tracker list.
- var items: [TrackerItem] = []
- for bookmark in self.bookmarks {
- items.append(TrackerItem(bookmark: bookmark))
}
- self.servers = items
+
+ self.servers = newItems
}
- .onOpenURL(perform: { url in
- if let s = Server(url: url) {
- openWindow(id: "server", value: s)
- }
- })
+
+ self.loading = false
}
+
}
#Preview {