Files
tether/ios/MacTetherApp/Sources/MenuView.swift
Patrick Ecord c4334143b0 Auto-derive room ID from iCloud account or device hardware
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>
2026-06-20 15:00:12 -05:00

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)
}
}