RoomIdentity.derive() in TetherKit: ubiquityIdentityToken (iCloud account, portable across devices) → IOKit/identifierForVendor (hardware, device-tied) → Keychain UUID (last resort). No user config needed. Both apps auto-connect on relaunch if server URL was previously saved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
3.6 KiB
Swift
102 lines
3.6 KiB
Swift
import SwiftUI
|
|
import TetherKit
|
|
|
|
struct MenuView: View {
|
|
@Bindable var store: MacTetherStore
|
|
@State private var draft = ""
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
header
|
|
Divider()
|
|
feedList
|
|
Divider()
|
|
composer
|
|
}
|
|
.padding(8)
|
|
.task { store.connectIfSaved() }
|
|
}
|
|
|
|
private var header: some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack {
|
|
Circle()
|
|
.fill(store.connected ? Color.green : Color.secondary)
|
|
.frame(width: 8, height: 8)
|
|
Text(store.connected ? "connected · \(store.room)" : "disconnected")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
Spacer()
|
|
Button(store.connected ? "Disconnect" : "Connect") {
|
|
store.connected ? store.disconnect() : store.connect()
|
|
}.font(.caption)
|
|
}
|
|
if !store.connected {
|
|
TextField("http://tether.lan:8765", text: $store.serverURL)
|
|
.textFieldStyle(.roundedBorder).font(.caption)
|
|
Button("Connect") { store.connect() }
|
|
.buttonStyle(.borderedProminent).controlSize(.small)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
if store.hasAttemptedConnect, let err = store.lastError {
|
|
Text(err).font(.caption2).foregroundStyle(.red)
|
|
}
|
|
}
|
|
.padding(.bottom, 6)
|
|
}
|
|
|
|
private var feedList: some View {
|
|
ScrollView {
|
|
LazyVStack(alignment: .leading, spacing: 4) {
|
|
if store.feed.isEmpty {
|
|
Text("No messages yet — clipboard changes auto-send when connected.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding(.top, 20)
|
|
} else {
|
|
ForEach(store.feed) { msg in
|
|
feedRow(msg)
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, 6)
|
|
}
|
|
.frame(minHeight: 120, maxHeight: 240)
|
|
}
|
|
|
|
private func feedRow(_ msg: Message) -> some View {
|
|
HStack(alignment: .top, spacing: 6) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(msg.text ?? "")
|
|
.font(.caption).lineLimit(3)
|
|
HStack {
|
|
Text(msg.source ?? "?").font(.caption2).foregroundStyle(.secondary)
|
|
Spacer()
|
|
Text(msg.date, style: .time).font(.caption2).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
Button { store.copyToPasteboard(msg) } label: {
|
|
Image(systemName: "doc.on.doc").font(.caption2)
|
|
}
|
|
.buttonStyle(.plain).foregroundStyle(.secondary)
|
|
}
|
|
.padding(6)
|
|
.background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 6))
|
|
}
|
|
|
|
private var composer: some View {
|
|
HStack(spacing: 6) {
|
|
TextField("Send text…", text: $draft)
|
|
.textFieldStyle(.roundedBorder).font(.caption)
|
|
.onSubmit { if !draft.isEmpty { store.send(draft); draft = "" } }
|
|
Button {
|
|
store.send(draft); draft = ""
|
|
} label: {
|
|
Image(systemName: "paperplane.fill")
|
|
}
|
|
.disabled(!store.connected || draft.isEmpty)
|
|
.buttonStyle(.borderedProminent).controlSize(.small)
|
|
}
|
|
.padding(.top, 6)
|
|
}
|
|
}
|