aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-12-20 12:37:29 -0800
committerDustin Mierau <dustin@mierau.me>2023-12-20 12:37:29 -0800
commit174e5554fc55f19b36b9fa3a3155d27ef918141d (patch)
treea4b4e009a284da8c8739d9a64638d5f3dafcd64f
parentf58b0da88077b3129c2321b4af82b807c6d38ebf (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.swift5
-rw-r--r--Hotline/macOS/FilesView.swift10
-rw-r--r--Hotline/macOS/NewsView.swift8
-rw-r--r--Hotline/macOS/TrackerView.swift432
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
}
}
}