diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-01-17 23:18:27 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-01-17 23:18:27 -0800 |
| commit | 1b303803e53dbeb24dab1a7c714d71ee14f002db (patch) | |
| tree | 4616861b23642d26c9b58b5925315232c1fdb975 /Hotline/macOS/TrackerView.swift | |
| parent | 8e85fbbdadcb993cd270ff4bf60d2280b9e7ad56 (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.swift | 568 |
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 { |