aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-11-07 21:03:53 -0800
committerDustin Mierau <dustin@mierau.me>2025-11-07 21:03:53 -0800
commit2f332ee497af925db1a2583135e9dfcdaff84794 (patch)
treeff22f357a87cfa9dd577b4273f7f62a89906d83b /Hotline/macOS
parent637cd9af54a58db0c5dc2062b6c20291afd31147 (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.swift19
-rw-r--r--Hotline/macOS/Trackers/TrackerItemView.swift2
-rw-r--r--Hotline/macOS/Trackers/TrackerView.swift114
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 {