mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-15 20:29:55 +00:00
197 lines
7.8 KiB
Swift
197 lines
7.8 KiB
Swift
import HapiClient
|
|
import SwiftUI
|
|
|
|
/// Manual pairing path for hubs without `--relay` (nothing to scan): type or
|
|
/// paste the hub URL and the access token the hub prints at startup, then
|
|
/// pair directly with inline progress and error states.
|
|
struct ManualEntryView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Environment(AppModel.self) private var model
|
|
@State private var attempt = PairingAttempt()
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ManualPairingFormView(isPairing: attempt.isPairing, failure: attempt.failure) {
|
|
attempt.failure = nil
|
|
} onPair: { submission in
|
|
attempt.pair(model, hubUrl: submission.hubURL, accessToken: submission.accessToken)
|
|
}
|
|
.navigationTitle("Enter Hub Details")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") {
|
|
dismiss()
|
|
}
|
|
.disabled(attempt.isPairing)
|
|
}
|
|
}
|
|
.interactiveDismissDisabled(attempt.isPairing)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The production form also runs without AppModel in app-hosted interaction
|
|
/// tests. Only the explicit submit action can invoke onPair.
|
|
struct ManualPairingFormView: View {
|
|
let isPairing: Bool
|
|
let failure: PairingFailure?
|
|
var onEdit: () -> Void
|
|
var onPair: (ManualPairingForm.Submission) -> Void
|
|
|
|
@Environment(\.dynamicTypeSize) private var typeSize
|
|
@Environment(\.locale) private var locale
|
|
@State private var form = ManualPairingForm()
|
|
@State private var focusedField: ManualPairingForm.Field? = .address
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
let layout = typeSize.isAccessibilitySize
|
|
? AnyLayout(VStackLayout(alignment: .leading, spacing: 0))
|
|
: AnyLayout(HStackLayout(spacing: 12))
|
|
layout {
|
|
Menu {
|
|
Picker("Connection protocol", selection: Binding(
|
|
get: { form.scheme },
|
|
set: { value in
|
|
guard !isPairing else { return }
|
|
form.selectScheme(value)
|
|
onEdit()
|
|
}
|
|
)) {
|
|
ForEach(ManualPairingForm.Scheme.allCases, id: \.self) { scheme in
|
|
Text(verbatim: scheme.prefix).tag(scheme)
|
|
}
|
|
}
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Text(verbatim: form.scheme.prefix)
|
|
Image(systemName: "chevron.down").font(.caption)
|
|
}
|
|
.fixedSize()
|
|
.frame(minHeight: 44)
|
|
}
|
|
.accessibilityLabel("Connection protocol")
|
|
.accessibilityValue(form.scheme.prefix)
|
|
.accessibilityIdentifier("pairing.scheme")
|
|
.disabled(isPairing)
|
|
|
|
input(.address)
|
|
}
|
|
inputError(for: .address)
|
|
} header: {
|
|
Text("Hub URL")
|
|
} footer: {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Enter your hub address without the protocol, or paste a full URL or pairing link.")
|
|
if form.scheme == .http {
|
|
Label("HTTP is unencrypted and may be restricted by iOS. HTTPS is recommended.", systemImage: "exclamationmark.triangle")
|
|
}
|
|
}
|
|
}
|
|
|
|
Section {
|
|
input(.accessToken)
|
|
inputError(for: .accessToken)
|
|
} header: {
|
|
Text("Access token")
|
|
} footer: {
|
|
Text("Printed by the hub at startup, or available in web Settings → Companion Pairing. You can also paste a pairing link here.")
|
|
}
|
|
|
|
if form.didImportLink {
|
|
Section {
|
|
Label("Pairing details filled in. Review them, then tap Pair.", systemImage: "checkmark.circle")
|
|
.foregroundStyle(.secondary)
|
|
.accessibilityIdentifier("pairing.imported")
|
|
}
|
|
}
|
|
|
|
if let failure {
|
|
Section { PairingErrorView(failure: failure) }
|
|
}
|
|
|
|
Section {
|
|
Button(action: pairNow) {
|
|
if isPairing {
|
|
HStack(spacing: 8) {
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
Text("Pairing…")
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
} else {
|
|
Text(failure == nil ? LocalizedStringKey("Pair") : LocalizedStringKey("Try Again"))
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
.accessibilityIdentifier("pairing.submit")
|
|
.disabled(form.submission == nil || isPairing)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func input(_ field: ManualPairingForm.Field) -> some View {
|
|
PairingTextField(
|
|
text: Binding(get: { form.text(for: field) }, set: { value in
|
|
guard !isPairing else { return }
|
|
form.edit(value, in: field)
|
|
onEdit()
|
|
}),
|
|
focusedField: $focusedField,
|
|
field: field,
|
|
placeholder: field == .address
|
|
? String(localized: LocalizedStringResource("Domain or IP address", locale: locale))
|
|
: String(localized: LocalizedStringResource("Access token", locale: locale)),
|
|
accessibilityLabel: field == .address
|
|
? String(localized: LocalizedStringResource("Hub URL", locale: locale))
|
|
: String(localized: LocalizedStringResource("Access token", locale: locale)),
|
|
isEnabled: !isPairing,
|
|
onPaste: { raw in
|
|
guard !isPairing else { return false }
|
|
let handled = form.paste(raw, into: field)
|
|
if handled { onEdit() }
|
|
return handled
|
|
},
|
|
onEndEditing: {
|
|
guard !isPairing else { return }
|
|
form.finishEditing(field)
|
|
},
|
|
onReturn: {
|
|
guard !isPairing else { return }
|
|
if field == .address {
|
|
form.finishEditing(field)
|
|
focusedField = .accessToken
|
|
} else {
|
|
pairNow()
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func inputError(for field: ManualPairingForm.Field) -> some View {
|
|
if let error = form.error(for: field) {
|
|
let message: LocalizedStringKey = switch error {
|
|
case .invalidAddress: "Enter a valid hub domain or IP address, with an optional port."
|
|
case .invalidLink: "This pairing link is invalid or incomplete. Copy the full link from your hub."
|
|
case .missingToken: "Enter the access token printed by your hub."
|
|
}
|
|
Label(message, systemImage: "exclamationmark.circle")
|
|
.font(.footnote)
|
|
.foregroundStyle(.red)
|
|
.accessibilityIdentifier(field == .address ? "pairing.addressError" : "pairing.tokenError")
|
|
}
|
|
}
|
|
|
|
private func pairNow() {
|
|
guard !isPairing else { return }
|
|
form.finishEditing(.address)
|
|
form.finishEditing(.accessToken)
|
|
guard let submission = form.submission else { return }
|
|
focusedField = nil
|
|
onPair(submission)
|
|
}
|
|
}
|