Settings Feature Enhancement
Nucleic-Session: 89BFAB13-D559-43FC-BE28-0B2639BECF0F Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -370,3 +370,118 @@ struct ManualPairingView: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// "Add a device to this mesh": the phone can't mint a join code itself (it runs no listener), so
|
||||||
|
/// it asks a connected Mac to open a pairing window and relays back that Mac's code — shown here as
|
||||||
|
/// a QR to scan from a new iPhone/iPad, or a copyable link to send to a new Mac. Scanning/pasting it
|
||||||
|
/// joins the new device to the whole group. Dismissing the sheet closes the Mac's pairing window
|
||||||
|
/// (`RemoteStore.cancelPairingCode`, wired to the sheet's `onDismiss`).
|
||||||
|
struct AddDeviceView: View {
|
||||||
|
@EnvironmentObject var store: RemoteStore
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State private var copied = false
|
||||||
|
@State private var copyResetTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
switch store.addDevice {
|
||||||
|
case .idle, .requesting:
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
ProgressView()
|
||||||
|
Text("Asking a Mac in your mesh for a join code…")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .unavailable:
|
||||||
|
Section {
|
||||||
|
Label("Couldn't get a join code", systemImage: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
Text("This needs a Mac in your mesh that's connected right now. Make sure "
|
||||||
|
+ "one is reachable with its meshing service on, then try again.")
|
||||||
|
.font(.callout).foregroundStyle(.secondary)
|
||||||
|
Button("Try again") { store.requestPairingCode() }
|
||||||
|
}
|
||||||
|
case .ready(let code):
|
||||||
|
codeSection(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Add a device")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fire the request as the sheet appears; `onDismiss` (on the presenter) tells the Mac to
|
||||||
|
// close its pairing window.
|
||||||
|
.onAppear { if case .ready = store.addDevice {} else { store.requestPairingCode() } }
|
||||||
|
.onDisappear { copyResetTask?.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func codeSection(_ code: String) -> some View {
|
||||||
|
Section {
|
||||||
|
if let image = Self.qrImage(code) {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Image(uiImage: image)
|
||||||
|
.interpolation(.none)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 220, height: 220)
|
||||||
|
.padding(10)
|
||||||
|
.background(.white, in: RoundedRectangle(cornerRadius: 12))
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Scan to join")
|
||||||
|
} footer: {
|
||||||
|
Text("On a new iPhone or iPad, open Nucleic Remote ▸ Join an Orbital mesh and scan this. "
|
||||||
|
+ "It joins the whole group — every Mac and device here.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Text(code)
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.lineLimit(2)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
Button {
|
||||||
|
copyCode(code)
|
||||||
|
} label: {
|
||||||
|
Label(copied ? "Copied" : "Copy join code",
|
||||||
|
systemImage: copied ? "checkmark" : "doc.on.doc")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Or send a code")
|
||||||
|
} footer: {
|
||||||
|
Text("Paste into “Join mesh…” on another Mac, or “Enter code manually” on an iPhone or "
|
||||||
|
+ "iPad. A Mac joining is confirmed on the Mac that shared the code.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func copyCode(_ code: String) {
|
||||||
|
UIPasteboard.general.string = code
|
||||||
|
withAnimation(.easeOut(duration: 0.12)) { copied = true }
|
||||||
|
copyResetTask?.cancel()
|
||||||
|
copyResetTask = Task {
|
||||||
|
try? await Task.sleep(for: .seconds(1.4))
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
withAnimation(.easeIn(duration: 0.2)) { copied = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the join string as a QR code (CoreImage), mirroring the Mac's pairing sheet.
|
||||||
|
private static func qrImage(_ string: String) -> UIImage? {
|
||||||
|
let filter = CIFilter.qrCodeGenerator()
|
||||||
|
filter.message = Data(string.utf8)
|
||||||
|
filter.correctionLevel = "M"
|
||||||
|
guard let output = filter.outputImage else { return nil }
|
||||||
|
let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
|
||||||
|
let context = CIContext()
|
||||||
|
guard let cg = context.createCGImage(scaled, from: scaled.extent) else { return nil }
|
||||||
|
return UIImage(cgImage: cg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user