38b9f310e2
ci / docs-site (push) Successful in 1m5s
apple / swift (push) Successful in 1m10s
ci / web (push) Successful in 1m12s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m46s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m57s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 53s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m10s
ci / bench (push) Successful in 5m43s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
release / apple (push) Successful in 8m10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
arch / build-publish (push) Successful in 11m59s
android / android (push) Successful in 13m3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m39s
docker / deploy-docs (push) Successful in 18s
apple / screenshots (push) Successful in 5m32s
deb / build-publish (push) Successful in 14m54s
ci / rust (push) Successful in 23m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m53s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m44s
flatpak / build-publish (push) Failing after 35s
Ship the Android client's 3-tier stats-overlay semantics in every other client (design/stats-unification.md vocabulary): Off → Compact (one line: fps · e2e ms · Mb/s + loss flag) → Normal (mode + e2e p50/p95 + loss counters) → Detailed (decoder path, HDR tag, per-stage latency equation). Apple: new StatsVerbosity in PunktfunkKit persisted under punktfunk.statsVerbosity (migrates the legacy hudEnabled bool: explicit off → Off, else Normal). The existing three-finger tap (TouchMouse, trackpad/pointer modes only — touch passthrough untouched) now cycles the tiers instead of toggling, matching Android; ⌃⌥⇧S (menu + captured-state monitor) cycles the same ladder. Tiered StreamHUDView (compact glass pill / headline HUD / full equation HUD); the iOS corner disconnect also shows in Compact (the pill carries no button). Tier pickers on iOS, macOS, tvOS and the gamepad settings UI. Session stack (Linux + Windows + Deck share punktfunk-session): shared pf_client_core::trust::StatsVerbosity; Settings grows stats_verbosity with a show_stats fallback, and writes keep the legacy bool in sync so pre-tier binaries reading the same JSON agree on off vs on. Ctrl+Alt+Shift+S cycles the tier and re-renders the OSD immediately from the last stats window; the stdout stats: line always carries the full Detailed text so the shell status card and scripts keep a stable shape; --stats bumps Off → Normal without demoting a richer tier. Tier pickers in the GTK dialog, the WinUI settings page and the console-UI settings row; shortcut copy updated (GTK shortcuts window, Windows help, session README). The Windows legacy builtin path keeps its bool HUD. Tests: tier migration/round-trip in trust.rs, tiered stats_text output in pf-presenter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
167 lines
6.7 KiB
Swift
167 lines
6.7 KiB
Swift
// SettingsView's footers and stateful helpers, used by both the section builders
|
|
// (SettingsView+Sections.swift) and the per-platform bodies (SettingsView.swift). The option
|
|
// LISTS live in SettingsOptions — they're shared with the gamepad settings screen too.
|
|
|
|
#if os(macOS)
|
|
import AppKit
|
|
#endif
|
|
import PunktfunkKit
|
|
import SwiftUI
|
|
|
|
extension SettingsView {
|
|
// MARK: - Bitrate
|
|
|
|
/// Slider domain, log-scale: the useful range spans three orders of magnitude
|
|
/// (a few Mbps … 3 Gbps) — linear would cram everything below 100 Mbps into the
|
|
/// first pixels.
|
|
private static let minSliderKbps = 2_000.0
|
|
private static let maxSliderKbps = 3_000_000.0
|
|
|
|
static let bitrateFooter =
|
|
"Automatic uses the host's default bitrate (20 Mbps); the host clamps any choice "
|
|
+ "to its supported range. Run a speed test from a host card's context menu to "
|
|
+ "pick an informed value. Applies from the next session."
|
|
|
|
static let gigabitWarning =
|
|
"Above 1 Gbps — test the network speed first (a host card's context menu → "
|
|
+ "Test Network Speed…). A bitrate beyond what the link sustains causes loss "
|
|
+ "and stutter."
|
|
|
|
/// `bitrateKbps == 0` is Automatic; switching to manual lands on the host default.
|
|
var automaticBitrate: Binding<Bool> {
|
|
Binding(
|
|
get: { bitrateKbps == 0 },
|
|
set: { bitrateKbps = $0 ? 0 : 20_000 })
|
|
}
|
|
|
|
/// Slider position 0...1 ↔ kbps on the log scale, snapped to two significant figures
|
|
/// so the readout shows round numbers instead of 47_322.
|
|
var bitrateSlider: Binding<Double> {
|
|
Binding(
|
|
get: {
|
|
let v = Double(bitrateKbps).clamped(Self.minSliderKbps, Self.maxSliderKbps)
|
|
return log(v / Self.minSliderKbps)
|
|
/ log(Self.maxSliderKbps / Self.minSliderKbps)
|
|
},
|
|
set: { pos in
|
|
let raw = Self.minSliderKbps
|
|
* pow(Self.maxSliderKbps / Self.minSliderKbps, pos)
|
|
let mag = pow(10, floor(log10(raw)) - 1)
|
|
bitrateKbps = Int((raw / mag).rounded() * mag)
|
|
})
|
|
}
|
|
|
|
// MARK: - Statistics
|
|
|
|
static var statisticsFooter: String {
|
|
let base = "Shows streaming statistics in the chosen corner — Compact is a one-line "
|
|
+ "pill, Normal adds resolution and latency, Detailed adds the latency stage "
|
|
+ "breakdown."
|
|
#if os(macOS)
|
|
return base + " ⌃⌥⇧S cycles Off → Compact → Normal → Detailed any time."
|
|
#elseif os(iOS)
|
|
return base + " ⌃⌥⇧S or a three-finger tap cycles Off → Compact → Normal → Detailed "
|
|
+ "any time."
|
|
#else
|
|
return base
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Audio
|
|
|
|
static var audioFooter: String {
|
|
#if os(macOS)
|
|
return "Host audio plays through the chosen speaker; your microphone feeds the host's "
|
|
+ "virtual mic. System default follows your Mac's device changes. Applies from the "
|
|
+ "next session."
|
|
#else
|
|
return "Host audio plays locally; your microphone feeds the host's virtual mic. "
|
|
+ "Applies from the next session."
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Controllers
|
|
|
|
static let controllersFooter =
|
|
"One controller is forwarded as player 1 — Automatic picks the most recently "
|
|
+ "connected. Type is the virtual pad the host creates; Automatic matches your "
|
|
+ "controller (a DualSense keeps adaptive triggers, lightbar, touchpad and motion). "
|
|
+ "Applies from the next session."
|
|
|
|
#if !os(tvOS)
|
|
static let gamepadUIFooter =
|
|
"When a controller connects, the host list and library switch to a controller-"
|
|
+ "friendly layout — larger focus targets and a swipeable cover browser. Turn this "
|
|
+ "off to always use the standard layout."
|
|
#endif
|
|
|
|
/// "Use controller" choices for this view's manager (see `SettingsOptions.controllerOptions`).
|
|
var controllerOptions: [(label: String, tag: String)] {
|
|
SettingsOptions.controllerOptions(gamepads)
|
|
}
|
|
|
|
func controllerRow(_ controller: GamepadManager.DiscoveredController) -> some View {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: controller.hasTouchpadAndMotion ? "playstation.logo" : "gamecontroller.fill")
|
|
.foregroundStyle(.secondary)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(controller.name)
|
|
HStack(spacing: 8) {
|
|
if !controller.isExtended {
|
|
Text(controller.productCategory)
|
|
}
|
|
if controller.hasAdaptiveTriggers {
|
|
Image(systemName: "r2.button.roundedtop.horizontal")
|
|
}
|
|
if controller.hasLight {
|
|
Image(systemName: "lightbulb.fill")
|
|
}
|
|
if controller.hasMotion {
|
|
Image(systemName: "gyroscope")
|
|
}
|
|
if controller.hasHaptics {
|
|
Image(systemName: "waveform")
|
|
}
|
|
if let level = controller.batteryLevel {
|
|
Text("\(Int(level * 100))%")
|
|
if controller.isCharging {
|
|
Image(systemName: "bolt.fill")
|
|
}
|
|
}
|
|
}
|
|
.font(.geist(11, relativeTo: .caption2))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
if gamepads.active?.id == controller.id {
|
|
Text("In use")
|
|
.font(.geist(11, .semibold, relativeTo: .caption2))
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 3)
|
|
.background(Capsule().fill(.green.opacity(0.2)))
|
|
.foregroundStyle(.green)
|
|
}
|
|
}
|
|
}
|
|
|
|
func fillFromMainScreen() {
|
|
#if os(macOS)
|
|
guard let screen = NSScreen.main else { return }
|
|
let scale = screen.backingScaleFactor
|
|
width = Int(screen.frame.width * scale)
|
|
height = Int(screen.frame.height * scale)
|
|
hz = screen.maximumFramesPerSecond
|
|
#else
|
|
// nativeBounds is portrait-oriented pixels — streams are landscape.
|
|
let bounds = UIScreen.main.nativeBounds
|
|
width = Int(max(bounds.width, bounds.height))
|
|
height = Int(min(bounds.width, bounds.height))
|
|
hz = UIScreen.main.maximumFramesPerSecond
|
|
#if os(iOS)
|
|
// The native mode is the "This device" wheel row, so leave Custom mode if it was on.
|
|
customMode = false
|
|
#endif
|
|
#endif
|
|
}
|
|
}
|