diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-12-20 12:37:29 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-12-20 12:37:29 -0800 |
| commit | 174e5554fc55f19b36b9fa3a3155d27ef918141d (patch) | |
| tree | a4b4e009a284da8c8739d9a64638d5f3dafcd64f | |
| parent | f58b0da88077b3129c2321b4af82b807c6d38ebf (diff) | |
Tracker window now allows for bookmarks and multiple trackers like the original Hotline client. You still can't add servers or trackers, but support is there. Also removed title from message reader view on macOS.
| -rw-r--r-- | Hotline/Application.swift | 5 | ||||
| -rw-r--r-- | Hotline/macOS/FilesView.swift | 10 | ||||
| -rw-r--r-- | Hotline/macOS/NewsView.swift | 8 | ||||
| -rw-r--r-- | Hotline/macOS/TrackerView.swift | 432 |
4 files changed, 334 insertions, 121 deletions
diff --git a/Hotline/Application.swift b/Hotline/Application.swift index 14717c2..93bf025 100644 --- a/Hotline/Application.swift +++ b/Hotline/Application.swift @@ -16,7 +16,11 @@ struct Application: App { #elseif os(macOS) WindowGroup { TrackerView() + .frame(minWidth: 250, minHeight: 250) } + .defaultSize(width: 700, height: 600) + .defaultPosition(.center) + WindowGroup(for: Server.self) { $server in if let s = server { ServerView(server: s) @@ -25,6 +29,7 @@ struct Application: App { } } .defaultSize(width: 700, height: 800) + .defaultPosition(.center) #endif } diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift index 534dd10..8482dd9 100644 --- a/Hotline/macOS/FilesView.swift +++ b/Hotline/macOS/FilesView.swift @@ -1,16 +1,6 @@ import SwiftUI import UniformTypeIdentifiers -@Observable -class FileSelection: Equatable { - var selectedFile: FileInfo? = nil - - static func == (lhs: FileSelection, rhs: FileSelection) -> Bool { - return lhs.selectedFile == rhs.selectedFile - } -} - - struct FileView: View { @Environment(Hotline.self) private var model: Hotline diff --git a/Hotline/macOS/NewsView.swift b/Hotline/macOS/NewsView.swift index b5752d3..aa32d6a 100644 --- a/Hotline/macOS/NewsView.swift +++ b/Hotline/macOS/NewsView.swift @@ -181,9 +181,9 @@ struct NewsView: View { if let news = selection { if news.type == .article { - Text(news.name).font(.title) - .textSelection(.enabled) - .padding(.bottom, 8) +// Text(news.name).font(.title) +// .textSelection(.enabled) +// .padding(.bottom, 8) if let poster = news.articleUsername, let postDate = news.articleDate { HStack(alignment: .firstTextBaseline) { @@ -208,7 +208,7 @@ struct NewsView: View { if let newsText = self.articleText { Text(newsText) .textSelection(.enabled) - .lineSpacing(9) + .lineSpacing(4) .padding(.top, 16) } } diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift index 684dd6a..d6473d9 100644 --- a/Hotline/macOS/TrackerView.swift +++ b/Hotline/macOS/TrackerView.swift @@ -1,39 +1,255 @@ import SwiftUI -struct TrackerView: View { +enum TrackerBookmarkType: String { + case tracker = "tracker" + case server = "server" +} + +struct TrackerBookmark { + let type: TrackerBookmarkType + let name: String + let address: String +} + +@Observable +class TrackerItem: Identifiable, Hashable { + @ObservationIgnored let id: UUID = UUID() + let bookmark: TrackerBookmark? + let server: Server? - // @Environment(\.modelContext) private var modelContext - // @Query private var items: [Item] + var servers: [TrackerItem]? -// @Environment(Hotline.self) private var model: Hotline - @Environment(\.colorScheme) var colorScheme - @Environment(\.openWindow) private var openWindow + var expanded: Bool = false + var loading: Bool = false - private var client = HotlineTrackerClient() + init(bookmark: TrackerBookmark) { + self.bookmark = bookmark + self.server = nil + self.servers = nil + + if bookmark.type == .tracker { + self.servers = [] + } + } + + init(server: Server) { + self.server = server + self.servers = nil + self.bookmark = nil + } + + static func == (lhs: TrackerItem, rhs: TrackerItem) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } + + func setServers(_ servers: [Server]) { + var newServers: [TrackerItem] = [] + + for server in servers { + newServers.append(TrackerItem(server: server)) + } + + self.servers = newServers + } @MainActor - func updateServers() async { - let fetchedServers: [HotlineServer] = await self.client.fetchServers(address: "hltracker.com", port: Tracker.defaultPort) + func loadServers() async { + guard + let bookmark = self.bookmark, + bookmark.type == .tracker + else { + self.loading = false + return + } - var newServers: [Server] = [] + let client = HotlineTrackerClient() + + + self.loading = true +// self.servers = [] + + let fetchedServers: [HotlineServer] = await client.fetchServers(address: bookmark.address, port: Tracker.defaultPort) + client.disconnect() + + var newItems: [TrackerItem] = [] + for s in fetchedServers { if let serverName = s.name { - newServers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))) + + let server = Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users)) +// let item = TrackerItem(server: server) + + newItems.append(TrackerItem(server: server)) } } + + self.servers = newItems - self.servers = newServers + self.loading = false } + +} + +struct TrackerItemView: View { +// @Environment(Hotline.self) private var model: Hotline -// private var model = Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient()) + @State var expanded = false + @State var loading = false - // @State private var tracker = Tracker(address: "hltracker.com", service: trackerService) + var item: TrackerItem + let depth: Int - @State private var servers: [Server] = [] + var body: some View { + HStack { + if + let bookmark = item.bookmark, + bookmark.type == .tracker { + Button { + item.expanded.toggle() + } label: { + Image(systemName: item.expanded ? "chevron.down" : "chevron.right") + .renderingMode(.template) + .frame(width: 10, height: 10) + .aspectRatio(contentMode: .fit) + .opacity(0.5) + } + .buttonStyle(.plain) + .frame(width: 12) + } + else { + HStack { + + }.frame(width: 12) + } + + HStack(alignment: .center) { + if let bookmark = item.bookmark { + switch bookmark.type { + case .tracker: + Image(systemName: "point.3.filled.connected.trianglepath.dotted") + case .server: + Image(systemName: "globe.americas.fill") + } + } + else if let _ = item.server { + Image(systemName: "globe.americas.fill") + } + } + .frame(width: 15) + + if let bookmark = item.bookmark { + switch bookmark.type { + case .tracker: + Text(bookmark.name).bold().lineLimit(1).truncationMode(.tail) + case .server: + Text(bookmark.name).lineLimit(1).truncationMode(.tail) + } + } + else if let server = item.server { + Text(server.name ?? server.address).lineLimit(1) + + if let description = server.description, !description.isEmpty { + Text(description).foregroundStyle(.secondary).lineLimit(1).truncationMode(.tail) + } + } + + if + let server = item.server, + server.users > 0 { + Spacer() + Text("\(server.users) \(Image(systemName: "person.fill"))") + .lineLimit(1) + .foregroundStyle(.secondary) + .padding([.leading, .trailing], 8) + .padding([.top, .bottom], 2) + .background(Capsule(style: .circular).stroke(.secondary.opacity(0.3), lineWidth: 1)) + } + else if + let bookmark = item.bookmark, + bookmark.type == .tracker { + if item.loading { + ProgressView() + .padding([.leading, .trailing], 2) + .controlSize(.small) + } + Spacer() + } + else { + Spacer() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.leading, CGFloat(depth * (12 + 10))) + .onChange(of: item.expanded) { + loading = false + + if + item.expanded, + let bookmark = item.bookmark, + bookmark.type == .tracker { + Task { + await item.loadServers() + } + } + } + + if + item.expanded, + let servers = item.servers { + ForEach(servers, id: \.self) { serverItem in + TrackerItemView(item: serverItem, depth: self.depth + 1).tag(serverItem) + } + } + } +} + +struct TrackerView: View { + @Environment(\.colorScheme) var colorScheme + @Environment(\.openWindow) private var openWindow + +// @AppStorage("servers", store: .standard) + var bookmarks: [TrackerBookmark] = [ + TrackerBookmark(type: .server, name: "Bob Kiwi's House", address: "73.132.92.104"), + TrackerBookmark(type: .tracker, name: "Featured Servers", address: "hltracker.com"), + TrackerBookmark(type: .tracker, name: "Ubersoft", address: "hotline.ubersoft.org") + + // "hltracker.com" + // "tracker.preterhuman.net" + // "hotline.ubersoft.org" + // "tracked.nailbat.com" + // "hotline.duckdns.org" + // "tracked.agent79.org" + + ] + + private var client = HotlineTrackerClient() + + @MainActor + func refresh() async { +// self.servers = [] +// +// let fetchedServers: [HotlineServer] = await self.client.fetchServers(address: "hltracker.com", port: Tracker.defaultPort) +// +// var newServers: [Server] = [] +// +// for s in fetchedServers { +// if let serverName = s.name { +// newServers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))) +// } +// } +// +// self.servers = newServers + } + + @State private var servers: [TrackerItem] = [] // @State private var selectedServer: Server? - @State private var selection: Server.ID? = nil + @State private var selection: TrackerItem? = nil @State private var scrollOffset: CGFloat = CGFloat.zero @State private var initialLoadComplete = false @@ -81,107 +297,109 @@ struct TrackerView: View { // } var body: some View { - // ZStack(alignment: .center) { - // VStack(alignment: .center) { - // ZStack(alignment: .top) { - // HStack(alignment: .center) { - // Button { - // connectVisible = true - // connectDismissed = false - // } label: { - // Text(Image(systemName: "gearshape.fill")) - // .symbolRenderingMode(.hierarchical) - // .foregroundColor(.primary) - // .font(.title2) - // .padding(.leading, 16) - // } - // .sheet(isPresented: $connectVisible) { - // connectDismissed = true - // } content: { - //// TrackerConnectView() - // } - // Spacer() - // } - // .frame(height: 40.0) - // Image("Hotline") - // .resizable() - // .renderingMode(.template) - // .foregroundColor(Color(hex: 0xE10000)) - // .scaledToFit() - // .frame(width: 40.0, height: 40.0) - // HStack(alignment: .center) { - // Spacer() - // Button { - // connectVisible = true - // connectDismissed = false - // } label: { - // Text(Image(systemName: "point.3.connected.trianglepath.dotted")) - // .symbolRenderingMode(.hierarchical) - // .foregroundColor(.primary) - // .font(.title2) - // .padding(.trailing, 16) - // } - // .sheet(isPresented: $connectVisible) { - // connectDismissed = true - // } content: { - // TrackerConnectView() - // } - // } - // .frame(height: 40.0) - // } - // .padding() - // - // Spacer() - // } - // .opacity(inverseLerp(lower: -50, upper: 0, v: scrollOffset)) - // .opacity(scrollOffset > 65 ? 0.0 : 1.0) - // .opacity(topBarOpacity) - // .zIndex(scrollOffset > 0 ? 1 : 3) - Table(of: Server.self, selection: $selection) { - TableColumn("Name") { server in - HStack { - Text(Image(systemName: "globe.americas.fill")) - Text(server.name!) + 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 server = item.server { + Button { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(server.address, forType: .string) + } label: { + Label("Copy Server Address", systemImage: "doc.on.doc") + } } } - .width(min: 80, ideal: 150) - - TableColumn("Status") { server in - if server.users > 0 { - Text("\(server.users)") - } - else { - Text("") - } - + } primaryAction: { items in + guard let clickedItem = items.first else { + return } - .width(50) - .alignment(.center) - TableColumn("Description") { server in - Text(server.description ?? "") + if + let bookmark = clickedItem.bookmark, + bookmark.type == .tracker { + clickedItem.expanded.toggle() + } + else if let server = clickedItem.server { + openWindow(value: server) } - } rows: { - ForEach(self.servers) { server in - TableRow(server) + else if + let bookmark = clickedItem.bookmark, + bookmark.type == .server { + let server = Server(name: bookmark.name, description: nil, address: bookmark.address, port: Server.defaultPort) + openWindow(value: server) } } - .contextMenu(forSelectionType: Server.ID.self) { items in - // ... - } primaryAction: { items in - guard - let selectionID = items.first, - let selectedServer = self.servers.first(where: { $0.id == selectionID }) - else { - return + .onKeyPress(.rightArrow) { + if + let selectedItem = selection, + let bookmark = selectedItem.bookmark, + bookmark.type == .tracker { + selectedItem.expanded = true + return .handled } - - openWindow(value: selectedServer) + return .ignored + } + .onKeyPress(.leftArrow) { + if + let selectedItem = selection, + let bookmark = selectedItem.bookmark, + bookmark.type == .tracker { + selectedItem.expanded = false + return .handled + } + return .ignored } .navigationTitle("Servers") - .task { - await updateServers() - initialLoadComplete = true + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + Task { + initialLoadComplete = false + await refresh() + initialLoadComplete = true + } + } label: { + Label("Refresh", systemImage: "arrow.clockwise") + } + } + + ToolbarItem(placement: .primaryAction) { + Button { + // Task { + // initialLoadComplete = false + // await updateServers() + // initialLoadComplete = true + // } + } label: { + Label("Add Tracker", systemImage: "point.3.connected.trianglepath.dotted") + } + } + + ToolbarItem(placement: .primaryAction) { + Button { + // Task { + // initialLoadComplete = false + // await updateServers() + // initialLoadComplete = true + // } + } label: { + Label("Add Server", systemImage: "plus") + } + } + } + .onAppear { + // Add initial items to tracker list. + var items: [TrackerItem] = [] + for bookmark in self.bookmarks { + items.append(TrackerItem(bookmark: bookmark)) + } + self.servers = items } } } |