diff options
| author | Dustin Mierau <dustin@mierau.me> | 2025-11-07 21:03:53 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2025-11-07 21:03:53 -0800 |
| commit | 2f332ee497af925db1a2583135e9dfcdaff84794 (patch) | |
| tree | ff22f357a87cfa9dd577b4273f7f62a89906d83b /Hotline/macOS | |
| parent | 637cd9af54a58db0c5dc2062b6c20291afd31147 (diff) | |
Add Bonjour discovery for Hotline servers on the local network to the Servers window.
Diffstat (limited to 'Hotline/macOS')
| -rw-r--r-- | Hotline/macOS/Trackers/BonjourServerRow.swift | 19 | ||||
| -rw-r--r-- | Hotline/macOS/Trackers/TrackerItemView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/Trackers/TrackerView.swift | 114 |
3 files changed, 131 insertions, 4 deletions
diff --git a/Hotline/macOS/Trackers/BonjourServerRow.swift b/Hotline/macOS/Trackers/BonjourServerRow.swift new file mode 100644 index 0000000..d99b23b --- /dev/null +++ b/Hotline/macOS/Trackers/BonjourServerRow.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct BonjourServerRow: View { + @Environment(\.openWindow) private var openWindow + + let server: BonjourState.BonjourServer + + var body: some View { + HStack(alignment: .center, spacing: 6) { + Image("Server") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16, alignment: .center) + Text(self.server.displayName).lineLimit(1).truncationMode(.tail) + Spacer(minLength: 0) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} diff --git a/Hotline/macOS/Trackers/TrackerItemView.swift b/Hotline/macOS/Trackers/TrackerItemView.swift index 6e62518..37093d2 100644 --- a/Hotline/macOS/Trackers/TrackerItemView.swift +++ b/Hotline/macOS/Trackers/TrackerItemView.swift @@ -36,7 +36,7 @@ struct TrackerItemView: View { if isLoading { ProgressView() .padding([.leading, .trailing], 2) - .controlSize(.small) + .controlSize(.mini) } Spacer(minLength: 0) if isExpanded && count > 0 { diff --git a/Hotline/macOS/Trackers/TrackerView.swift b/Hotline/macOS/Trackers/TrackerView.swift index ddc0a67..0eb4a12 100644 --- a/Hotline/macOS/Trackers/TrackerView.swift +++ b/Hotline/macOS/Trackers/TrackerView.swift @@ -6,11 +6,15 @@ import UniformTypeIdentifiers enum TrackerSelection: Hashable { case bookmark(Bookmark) case bookmarkServer(BookmarkServer) + case bonjourGroup + case bonjourServer(BonjourState.BonjourServer) var server: Server? { switch self { - case .bookmark(let b): return b.server - case .bookmarkServer(let t): return t.server + case .bookmark(let b): b.server + case .bookmarkServer(let t): t.server + case .bonjourGroup: nil + case .bonjourServer(let b): b.server } } } @@ -85,6 +89,62 @@ struct TrackerView: View { } } } + + var bonjourRowView: some View { + HStack(alignment: .center, spacing: 6) { + Button { + AppState.shared.bonjourState.isExpanded.toggle() + } label: { + Text(Image(systemName: AppState.shared.bonjourState.isExpanded ? "chevron.down" : "chevron.right")) + .bold() + .font(.system(size: 10)) + .opacity(0.5) + .frame(alignment: .center) + } + .buttonStyle(.plain) + .frame(width: 10) + .padding(.leading, 4) + .padding(.trailing, 2) + + Image(systemName: "bonjour") + .resizable() + .scaledToFit() + .symbolRenderingMode(.multicolor) + .frame(width: 16, height: 16, alignment: .center) + Text("Bonjour").bold().lineLimit(1).truncationMode(.tail) + + if AppState.shared.bonjourState.isBrowsing { + ProgressView() + .controlSize(.mini) + } + + Spacer(minLength: 0) + + if AppState.shared.bonjourState.isExpanded && !AppState.shared.bonjourState.discoveredServers.isEmpty { + HStack(spacing: 4) { + Text(String(AppState.shared.bonjourState.discoveredServers.count)) + + SpinningGlobeView() + .fontWeight(.semibold) + .frame(width: 12, height: 12) + } + .padding(.horizontal, 6) + .padding(.vertical, 2) + .foregroundStyle(.secondary) +// .background(.quinary) + .clipShape(.capsule) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onChange(of: AppState.shared.bonjourState.isExpanded) { oldState, newState in + if newState { + AppState.shared.bonjourState.startBrowsing() + } + else { + AppState.shared.bonjourState.stopBrowsing() + } + } + } var body: some View { List(selection: $selection) { @@ -115,6 +175,17 @@ struct TrackerView: View { .onDelete { deletedIndexes in Bookmark.delete(at: deletedIndexes, context: modelContext) } + + self.bonjourRowView + .tag(TrackerSelection.bonjourGroup) + + if AppState.shared.bonjourState.isExpanded { + ForEach(AppState.shared.bonjourState.discoveredServers, id: \.self) { record in + BonjourServerRow(server: record) + .tag(TrackerSelection.bonjourServer(record)) + .padding(.leading, 16 + 8 + 10) + } + } } .onDeleteCommand { switch self.selection { @@ -155,6 +226,10 @@ struct TrackerView: View { self.bookmarkContextMenu(bookmark) case .bookmarkServer(let server): self.bookmarkServerContextMenu(server) + case .bonjourGroup: + EmptyView() + case .bonjourServer(let bonjourServer): + self.bonjourServerContextMenu(bonjourServer) } } } primaryAction: { items in @@ -180,6 +255,15 @@ struct TrackerView: View { case .bookmarkServer(let bookmarkServer): openWindow(id: "server", value: bookmarkServer.server) + + case .bonjourGroup: + AppState.shared.bonjourState.isExpanded.toggle() + + case .bonjourServer(let bonjourServer): + if let server = bonjourServer.server { + openWindow(id: "server", value: server) + } + } } .fileExporter(isPresented: $bookmarkExportActive, document: bookmarkExport, contentTypes: [.data], defaultFilename: "\(bookmarkExport?.bookmark.name ?? "Hotline Bookmark").hlbm", onCompletion: { result in @@ -380,8 +464,32 @@ struct TrackerView: View { Label(bookmark.type == .tracker ? "Delete Tracker" : "Delete Bookmark", systemImage: "trash") } } - + @ViewBuilder + func bonjourServerContextMenu(_ bonjourServer: BonjourState.BonjourServer) -> some View { + Button { + guard let server = bonjourServer.server else { + return + } + let newBookmark = Bookmark(type: .server, name: server.name ?? server.address, address: server.address, port: server.port, login: nil, password: nil) + Bookmark.add(newBookmark, context: modelContext) + } label: { + Label("Bookmark", systemImage: "bookmark") + } + + Divider() + + Button { + guard let server = bonjourServer.server else { + return + } + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(server.displayAddress, forType: .string) + } label: { + Label("Copy Address", systemImage: "doc.on.doc") + } + } + func refresh() { // When a tracker is selected, refresh only that tracker. if let trackerSelection = self.selection { |