fix build
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
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
This commit is contained in:
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "swift",
|
||||
"request": "launch",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder:punktfunk}/clients/apple",
|
||||
"name": "Debug PunktfunkClient (clients/apple)",
|
||||
"target": "PunktfunkClient",
|
||||
"configuration": "debug",
|
||||
"preLaunchTask": "swift: Build Debug PunktfunkClient (clients/apple)"
|
||||
},
|
||||
{
|
||||
"type": "swift",
|
||||
"request": "launch",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder:punktfunk}/clients/apple",
|
||||
"name": "Release PunktfunkClient (clients/apple)",
|
||||
"target": "PunktfunkClient",
|
||||
"configuration": "release",
|
||||
"preLaunchTask": "swift: Build Release PunktfunkClient (clients/apple)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -425,6 +425,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/Punktfunk.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = F4H37KF6WC;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Config/Info.plist;
|
||||
@@ -463,6 +464,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/Punktfunk.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = F4H37KF6WC;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Config/Info.plist;
|
||||
@@ -500,6 +502,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
CODE_SIGN_ENTITLEMENTS = Config/Punktfunk.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = F4H37KF6WC;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Config/Info.plist;
|
||||
@@ -529,6 +532,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
CODE_SIGN_ENTITLEMENTS = Config/Punktfunk.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = F4H37KF6WC;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Config/Info.plist;
|
||||
|
||||
@@ -81,24 +81,50 @@ struct AddHostSheet: View {
|
||||
#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() }
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
Spacer()
|
||||
Button("Add Host") { add() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
.glassProminentButtonStyle()
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(address.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
}
|
||||
#if os(iOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
.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)
|
||||
|
||||
@@ -205,7 +205,13 @@ struct ContentView: View {
|
||||
Image(systemName: "xmark")
|
||||
.font(.headline.weight(.semibold))
|
||||
.frame(width: 36, height: 36)
|
||||
.background(.regularMaterial, in: Circle())
|
||||
// Sole touch exit when the HUD is off — a floating glass disc
|
||||
// over the frame (26+, material fallback). interactive: the disc
|
||||
// IS the tap target, so the glass reacts to press.
|
||||
.glassBackground(Circle(), interactive: true)
|
||||
// Match the hit region to the visible disc so every tap also
|
||||
// triggers the interactive-glass press highlight.
|
||||
.contentShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(12)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// GlassStyle.swift — the app's single, availability-gated entry point to Apple's "Liquid
|
||||
// Glass" (iOS / macOS / tvOS 26). Every Liquid Glass symbol (glassEffect, Glass, the
|
||||
// .glassProminent button style …) is HARD-gated to OS 26: referencing one with our
|
||||
// deployment targets (macOS 14 / iOS 17 / tvOS 17) is a COMPILE error, not a silent no-op,
|
||||
// unless it sits behind `if #available`. So all glass in the app routes through the two
|
||||
// helpers below, each of which falls back to the EXACT look the app shipped before
|
||||
// (.regularMaterial / .borderedProminent) — nothing regresses on older OSes, and the gating
|
||||
// lives in exactly one file.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Glass background
|
||||
|
||||
/// Liquid Glass behind a floating / overlay surface, with the pre-26 `.regularMaterial`
|
||||
/// look as the fallback. Use ONLY on the floating control / overlay layer (the streaming
|
||||
/// HUD, the trust card, the touch exit chip) — never on content tiles or dense forms (HIG).
|
||||
///
|
||||
/// `glassEffect()`'s own default shape is a Capsule, so panels MUST pass an explicit shape
|
||||
/// (a RoundedRectangle / Circle) or they render as a pill. `interactive` makes the glass
|
||||
/// react to press — only meaningful when the glass itself is the tap target.
|
||||
private struct GlassBackground<S: Shape>: ViewModifier {
|
||||
let shape: S
|
||||
var interactive = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 26, macOS 26, tvOS 26, *) {
|
||||
content.glassEffect(interactive ? .regular.interactive() : .regular, in: shape)
|
||||
} else {
|
||||
content.background(.regularMaterial, in: shape)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Liquid Glass (26+) or the existing `.regularMaterial` (pre-26) behind a floating
|
||||
/// surface. Pass the surface's shape explicitly — glass defaults to a Capsule otherwise.
|
||||
func glassBackground<S: Shape>(_ shape: S, interactive: Bool = false) -> some View {
|
||||
modifier(GlassBackground(shape: shape, interactive: interactive))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Glass primary button
|
||||
|
||||
/// The single prominent action on a floating / overlay or sheet surface: the Liquid-Glass
|
||||
/// prominent button style on 26+, falling back to `.borderedProminent` (the app's current
|
||||
/// primary style) below. Apply directly to a `Button`; role / keyboardShortcut / disabled
|
||||
/// chain after it as usual. tvOS stays `.borderedProminent` always — glass chrome fights the
|
||||
/// focus engine, and keeping it preserves today's tvOS look exactly.
|
||||
private struct GlassProminentButton: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
#if os(tvOS)
|
||||
content.buttonStyle(.borderedProminent)
|
||||
#else
|
||||
if #available(iOS 26, macOS 26, *) {
|
||||
content.buttonStyle(.glassProminent)
|
||||
} else {
|
||||
content.buttonStyle(.borderedProminent)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Liquid-Glass prominent style (26+, non-tvOS) or `.borderedProminent`. Drop-in for the
|
||||
/// `.buttonStyle(.borderedProminent)` on a surface's primary action.
|
||||
func glassProminentButtonStyle() -> some View {
|
||||
modifier(GlassProminentButton())
|
||||
}
|
||||
}
|
||||
@@ -217,7 +217,7 @@ struct HomeView: View {
|
||||
Text("Add your punktfunk host with the + button.")
|
||||
} actions: {
|
||||
Button("Add Host") { showAddHost = true }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassProminentButtonStyle()
|
||||
#if os(iOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
|
||||
@@ -88,6 +88,8 @@ struct HostCardView: View {
|
||||
#if !os(tvOS)
|
||||
// tvOS: the .card button style owns platter + focus motion — extra chrome
|
||||
// inside it mutes the grow/tilt. Material + accent ring are for pointer UIs.
|
||||
// Deliberately .regularMaterial, not Liquid Glass: HIG keeps glass off content
|
||||
// tiles (it flattens hierarchy over an opaque grid) — see GlassStyle.swift.
|
||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14))
|
||||
.overlay {
|
||||
if isMostRecent {
|
||||
|
||||
@@ -81,7 +81,7 @@ struct LibraryView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: 420)
|
||||
Button("Retry") { Task { await load() } }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassProminentButtonStyle()
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
@@ -150,7 +150,7 @@ struct PairSheet: View {
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
Button("Pair & Connect") { runCeremony() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassProminentButtonStyle()
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
@@ -165,6 +165,15 @@ struct PairSheet: View {
|
||||
.frame(width: 400)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
// Bottom sheet instead of a full-screen modal (Liquid Glass background on iOS 26).
|
||||
// .medium rests; .large is included so the sheet grows to keep the Pair/Cancel row
|
||||
// above the keyboard when the PIN field is focused. Hide the grabber while the ceremony
|
||||
// is in flight — dismissal is disabled then (interactiveDismissDisabled), so a drag
|
||||
// would only rubber-band; the always-enabled Cancel button is the exit.
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(busy ? .hidden : .visible)
|
||||
#endif
|
||||
.interactiveDismissDisabled(busy)
|
||||
.onDisappear { token.cancelled = true } // any other dismissal path
|
||||
#endif
|
||||
|
||||
@@ -91,14 +91,14 @@ struct SpeedTestSheet: View {
|
||||
bitrateKbps = rec
|
||||
dismiss()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassProminentButtonStyle()
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
}
|
||||
if case .failed = phase {
|
||||
Button("Retry") { run() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassProminentButtonStyle()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,12 @@ struct SpeedTestSheet: View {
|
||||
.frame(width: 420)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
// Bottom sheet rather than a full-screen modal; .medium stays put as the result view
|
||||
// swaps in (a measured height would resize the sheet mid-probe).
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
#endif
|
||||
.onAppear { run() }
|
||||
.onDisappear { token.cancelled = true }
|
||||
}
|
||||
|
||||
@@ -99,7 +99,9 @@ struct StreamHUDView: View {
|
||||
#endif
|
||||
}
|
||||
.padding(10)
|
||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
|
||||
// Floating HUD over live video — the canonical Liquid-Glass overlay surface (26+);
|
||||
// falls back to .regularMaterial below 26 (see GlassStyle).
|
||||
.glassBackground(RoundedRectangle(cornerRadius: 10))
|
||||
.padding(10)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ struct TrustCardView: View {
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
Button("Trust & Connect", action: onTrust)
|
||||
// Opaque prominent, NOT glass: this card is itself a glass panel
|
||||
// (.glassBackground below), and glass-on-glass loses contrast — a tinted
|
||||
// bordered button reads cleanly over glass (HIG). The sheet primaries stay
|
||||
// glass because the system manages the sheet's own glass layering.
|
||||
.buttonStyle(.borderedProminent)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
@@ -58,7 +62,9 @@ struct TrustCardView: View {
|
||||
}
|
||||
.padding(28)
|
||||
.frame(maxWidth: 440)
|
||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 18))
|
||||
// Floating trust card over the blurred stream — Liquid Glass on 26+, .regularMaterial
|
||||
// fallback below. The inner fingerprint box stays .quaternary (content, not glass).
|
||||
.glassBackground(RoundedRectangle(cornerRadius: 18))
|
||||
}
|
||||
|
||||
/// 64 hex chars → four groups per line, two lines — easy to eyeball against the log.
|
||||
|
||||
@@ -145,4 +145,23 @@ fi
|
||||
|
||||
rm -rf clients/apple/PunktfunkCore.xcframework
|
||||
"${XCODEBUILD[@]}" -create-xcframework "${ARGS[@]}" -output clients/apple/PunktfunkCore.xcframework
|
||||
echo "OK: clients/apple/PunktfunkCore.xcframework"
|
||||
|
||||
# Xcode (unlike `swift build`) refuses to EMBED an unsigned xcframework: the app targets in
|
||||
# Punktfunk.xcodeproj fail with "The framework 'PunktfunkCore.xcframework' is unsigned". So
|
||||
# sign the bundle here. Identity: $CODESIGN_IDENTITY if set, else the first "Apple Development"
|
||||
# identity in the keychain, else ad-hoc ("-") — ad-hoc satisfies `swift build` and most local
|
||||
# Xcode runs; a real identity is needed for device/distribution. --timestamp=none keeps it
|
||||
# offline (a secure timestamp only matters for notarized distribution, which re-signs anyway).
|
||||
SIGN_ID="${CODESIGN_IDENTITY:-}"
|
||||
if [[ -z "$SIGN_ID" ]]; then
|
||||
SIGN_ID=$(security find-identity -v -p codesigning 2>/dev/null \
|
||||
| awk -F'"' '/Apple Development/ {print $2; exit}')
|
||||
fi
|
||||
SIGN_ID="${SIGN_ID:--}" # ad-hoc fallback when no real identity is available
|
||||
if codesign --force --timestamp=none --sign "$SIGN_ID" clients/apple/PunktfunkCore.xcframework; then
|
||||
echo "OK: clients/apple/PunktfunkCore.xcframework (signed: $SIGN_ID)"
|
||||
else
|
||||
echo "WARN: clients/apple/PunktfunkCore.xcframework built but NOT signed — Xcode app" >&2
|
||||
echo " builds will report it unsigned. Set CODESIGN_IDENTITY and re-run." >&2
|
||||
echo "OK: clients/apple/PunktfunkCore.xcframework (unsigned)"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user