86979d0abc
apple / swift (push) Successful in 55s
ci / rust (push) Successful in 1m16s
ci / web (push) Successful in 33s
ci / docs-site (push) Successful in 29s
android / android (push) Successful in 3m18s
deb / build-publish (push) Successful in 3m7s
decky / build-publish (push) Successful in 12s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
ci / bench (push) Successful in 4m32s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m47s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m50s
docker / deploy-docs (push) Successful in 35s
improve iOS & iPadOS UI
143 lines
6.0 KiB
Swift
143 lines
6.0 KiB
Swift
// "+" sheet: name (optional) + address + port → a card in the hosts grid. The first
|
|
// actual connection runs the trust-on-first-use fingerprint prompt.
|
|
|
|
import SwiftUI
|
|
|
|
struct AddHostSheet: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var name = ""
|
|
@State private var address = ""
|
|
@State private var port = 9777
|
|
#if os(tvOS)
|
|
private enum EditField: String, Identifiable {
|
|
case name, address, port
|
|
var id: String { rawValue }
|
|
}
|
|
@State private var editing: EditField?
|
|
#endif
|
|
|
|
let onAdd: (StoredHost) -> Void
|
|
|
|
var body: some View {
|
|
#if os(tvOS)
|
|
// No inline text editing on tvOS — Settings-style value rows; pressing one
|
|
// raises the SYSTEM fullscreen keyboard (TVTextEntry).
|
|
VStack(spacing: 24) {
|
|
TVFieldRow(
|
|
label: "Name", value: name, placeholder: "Optional"
|
|
) { editing = .name }
|
|
TVFieldRow(
|
|
label: "Address", value: address, placeholder: "IP or hostname"
|
|
) { editing = .address }
|
|
TVFieldRow(
|
|
label: "Port", value: String(port), placeholder: ""
|
|
) { editing = .port }
|
|
HStack(spacing: 32) {
|
|
Button("Cancel", role: .cancel) { dismiss() }
|
|
Button("Add Host") { add() }
|
|
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
.padding(.top, 12)
|
|
}
|
|
.frame(maxWidth: 1000)
|
|
.padding(60)
|
|
.navigationTitle("Add Host")
|
|
.fullScreenCover(item: $editing) { field in
|
|
switch field {
|
|
case .name:
|
|
TVTextEntry(title: "Name (optional, e.g. Living Room)", text: name) {
|
|
name = $0
|
|
editing = nil
|
|
}
|
|
case .address:
|
|
TVTextEntry(title: "IP or hostname", text: address) {
|
|
address = $0.trimmingCharacters(in: .whitespaces)
|
|
editing = nil
|
|
}
|
|
case .port:
|
|
TVTextEntry(
|
|
title: "Port", text: String(port), keyboardType: .numberPad
|
|
) {
|
|
if let value = Int($0), (1...65535).contains(value) {
|
|
port = value
|
|
}
|
|
editing = nil
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
VStack(spacing: 0) {
|
|
Form {
|
|
TextField("Name", text: $name, prompt: Text("Optional — e.g. Living Room"))
|
|
TextField("Address", text: $address, prompt: Text("IP or hostname"))
|
|
TextField("Port", value: $port, format: .number.grouping(.never))
|
|
#if os(tvOS)
|
|
// tvOS floats the label above a non-empty field INSIDE the pill,
|
|
// shoving the value off-center — the field is always prefilled
|
|
// here, so drop the label there.
|
|
.labelsHidden()
|
|
#endif
|
|
}
|
|
#if !os(tvOS)
|
|
.formStyle(.grouped)
|
|
#endif
|
|
#if os(macOS)
|
|
// macOS: UNCHANGED — Cancel + Spacer + Add in an HStack, both wired to the
|
|
// window's default/cancel keyboard actions. The 380-wide .fixedSize panel below
|
|
// keeps this compact and centered.
|
|
HStack {
|
|
Button("Cancel", role: .cancel) { dismiss() }
|
|
.keyboardShortcut(.cancelAction)
|
|
Spacer()
|
|
Button("Add Host") { add() }
|
|
.glassProminentButtonStyle()
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
.padding(16)
|
|
#else
|
|
// iOS / iPadOS: NO Cancel — the sheet is dismissed by the drag indicator,
|
|
// swipe-down, or tap-outside. (AddHostSheet never sets interactiveDismissDisabled,
|
|
// so all three are live; if anyone adds it later, restore a Cancel here or there is
|
|
// no way back out.) A single FULL-WIDTH primary action reads as the one thing to do.
|
|
// The fill must be on the LABEL, not the Button: .frame(maxWidth:.infinity) on the
|
|
// Button only widens its hit area and leaves the styled capsule hugging the text —
|
|
// stretching the label is what makes the glass/bordered pill itself go edge-to-edge.
|
|
// .controlSize(.large) gives the tall, thumb-friendly height; .defaultAction lets a
|
|
// hardware keyboard / iPad Return submit.
|
|
Button { add() } label: {
|
|
Text("Add Host").frame(maxWidth: .infinity)
|
|
}
|
|
.glassProminentButtonStyle()
|
|
.controlSize(.large)
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
.padding(16)
|
|
#endif
|
|
}
|
|
#if os(iOS)
|
|
// A short bottom sheet, not a full-screen modal. .height(320) hugs the 3-field grouped
|
|
// Form + the full-width action row, instead of the half-screen .medium it used to rest
|
|
// at. A single fixed detent is enough: the system keeps the content above the keyboard
|
|
// when Address/Port is focused, and on iPadOS this renders as a short bottom sheet (not a
|
|
// centered formSheet card). If Dynamic Type grows the rows past this height the Form just
|
|
// scrolls inside the detent — nothing is clipped. (.height(_:) is iOS 16+, safe at iOS 17.)
|
|
.presentationDetents([.height(320)])
|
|
.presentationDragIndicator(.visible)
|
|
#endif
|
|
#if os(macOS)
|
|
.frame(width: 380)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
private func add() {
|
|
onAdd(StoredHost(
|
|
name: name.trimmingCharacters(in: .whitespaces),
|
|
address: address.trimmingCharacters(in: .whitespaces),
|
|
port: UInt16(clamping: port)))
|
|
dismiss()
|
|
}
|
|
}
|