fix(apple/ios): split streamModeSection — the inline iOS branch blew the type-checker budget
release / apple (push) Successful in 6m29s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m13s
apple / swift (push) Successful in 1m5s
apple / screenshots (push) Successful in 4m5s
ci / bench (push) Successful in 4m36s
ci / rust (push) Successful in 11m23s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 4m26s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m45s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m37s
android / android (push) Successful in 9m37s
docker / deploy-docs (push) Successful in 19s
release / apple (push) Successful in 6m29s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m13s
apple / swift (push) Successful in 1m5s
apple / screenshots (push) Successful in 4m5s
ci / bench (push) Successful in 4m36s
ci / rust (push) Successful in 11m23s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 4m26s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m45s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m37s
android / android (push) Successful in 9m37s
docker / deploy-docs (push) Successful in 19s
The Section's iOS content (resolution wheel + 3-way refresh rows + bitrate rows) as ONE ViewBuilder expression hit "the compiler is unable to type-check this expression in reasonable time" — failing exactly one build slice, the iOS archive, so swift test (macOS) and the tvOS/macOS archives never saw it and the 0.6.0 iOS TestFlight upload soft-failed. Extracted iosResolutionWheel / iosRefreshRows / bitrateRows; no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,63 +7,15 @@ import SwiftUI
|
|||||||
extension SettingsView {
|
extension SettingsView {
|
||||||
// MARK: - Sections (shared)
|
// MARK: - Sections (shared)
|
||||||
|
|
||||||
|
// NOTE: the Section content is deliberately split into the small named builders below — as one
|
||||||
|
// inline expression the iOS branch (wheel + 3-way refresh + bitrate rows) blew Swift's
|
||||||
|
// type-checker budget ("unable to type-check this expression in reasonable time"), which
|
||||||
|
// failed exactly one slice: the iOS archive (macOS/tvOS never compile that branch).
|
||||||
@ViewBuilder var streamModeSection: some View {
|
@ViewBuilder var streamModeSection: some View {
|
||||||
Section {
|
Section {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
// Touch-first: a rotating wheel of common resolutions (this device's own mode first) and
|
iosResolutionWheel
|
||||||
// a segmented refresh-rate control — the same family as the Clock/Timer pickers. The host
|
iosRefreshRows
|
||||||
// renders a virtual output at exactly the chosen mode, so these are real pixel sizes. The
|
|
||||||
// last wheel row, "Custom…", reveals width/height/refresh fields for an arbitrary mode.
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Resolution")
|
|
||||||
.font(.geist(15, relativeTo: .subheadline))
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Picker("Resolution", selection: resolutionSelection) {
|
|
||||||
ForEach(resolutionChoices, id: \.tag) { choice in
|
|
||||||
Text(choice.label).tag(choice.tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
.pickerStyle(.wheel)
|
|
||||||
.frame(maxHeight: 140)
|
|
||||||
}
|
|
||||||
if isCustomResolution {
|
|
||||||
// Arbitrary entry: type the exact width × height (and refresh) the host should drive.
|
|
||||||
HStack {
|
|
||||||
TextField("Width", value: $width, format: .number.grouping(.never))
|
|
||||||
.keyboardType(.numberPad)
|
|
||||||
Text("×")
|
|
||||||
TextField("Height", value: $height, format: .number.grouping(.never))
|
|
||||||
.labelsHidden()
|
|
||||||
.keyboardType(.numberPad)
|
|
||||||
}
|
|
||||||
// A row built from an HStack of TextFields otherwise insets its bottom separator to
|
|
||||||
// the inner content, clipping the hairline under "Width"; pin it to the cell edge.
|
|
||||||
.alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
|
|
||||||
LabeledContent("Refresh rate") {
|
|
||||||
TextField("Hz", value: $hz, format: .number.grouping(.never))
|
|
||||||
.keyboardType(.numberPad)
|
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
}
|
|
||||||
} else if refreshChoices.count > 1 {
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
Text("Refresh rate")
|
|
||||||
.font(.geist(15, relativeTo: .subheadline))
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Picker("Refresh rate", selection: $hz) {
|
|
||||||
ForEach(refreshChoices, id: \.self) { rate in
|
|
||||||
Text("\(rate) Hz").tag(rate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// A device with a single supported rate (e.g. 60 Hz) has nothing to pick.
|
|
||||||
LabeledContent("Refresh rate") {
|
|
||||||
Text("\(hz) Hz").foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button("Use this display's mode") { fillFromMainScreen() }
|
Button("Use this display's mode") { fillFromMainScreen() }
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
HStack {
|
HStack {
|
||||||
@@ -78,23 +30,7 @@ extension SettingsView {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
Toggle("Automatic bitrate", isOn: automaticBitrate)
|
bitrateRows
|
||||||
if bitrateKbps != 0 {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Slider(value: bitrateSlider, in: 0...1) {
|
|
||||||
Text("Bitrate")
|
|
||||||
}
|
|
||||||
Text(SpeedTestSheet.mbpsLabel(kbps: bitrateKbps))
|
|
||||||
.monospacedDigit()
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.frame(minWidth: 76, alignment: .trailing)
|
|
||||||
}
|
|
||||||
if bitrateKbps > 1_000_000 {
|
|
||||||
Label(Self.gigabitWarning, systemImage: "exclamationmark.triangle.fill")
|
|
||||||
.font(.geist(12, relativeTo: .caption))
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
} header: {
|
} header: {
|
||||||
Text("Stream mode")
|
Text("Stream mode")
|
||||||
@@ -109,6 +45,67 @@ extension SettingsView {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
// MARK: - Stream mode (iOS wheel)
|
// MARK: - Stream mode (iOS wheel)
|
||||||
|
|
||||||
|
/// Touch-first: a rotating wheel of common resolutions (this device's own mode first) — the
|
||||||
|
/// same family as the Clock/Timer pickers. The host renders a virtual output at exactly the
|
||||||
|
/// chosen mode, so these are real pixel sizes. The last wheel row, "Custom…", reveals
|
||||||
|
/// width/height/refresh fields for an arbitrary mode (see `iosRefreshRows`).
|
||||||
|
@ViewBuilder private var iosResolutionWheel: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Resolution")
|
||||||
|
.font(.geist(15, relativeTo: .subheadline))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Picker("Resolution", selection: resolutionSelection) {
|
||||||
|
ForEach(resolutionChoices, id: \.tag) { choice in
|
||||||
|
Text(choice.label).tag(choice.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(maxHeight: 140)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom W×H(+Hz) fields, a segmented refresh picker, or a static single-rate row.
|
||||||
|
@ViewBuilder private var iosRefreshRows: some View {
|
||||||
|
if isCustomResolution {
|
||||||
|
// Arbitrary entry: type the exact width × height (and refresh) the host should drive.
|
||||||
|
HStack {
|
||||||
|
TextField("Width", value: $width, format: .number.grouping(.never))
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
Text("×")
|
||||||
|
TextField("Height", value: $height, format: .number.grouping(.never))
|
||||||
|
.labelsHidden()
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
}
|
||||||
|
// A row built from an HStack of TextFields otherwise insets its bottom separator to
|
||||||
|
// the inner content, clipping the hairline under "Width"; pin it to the cell edge.
|
||||||
|
.alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
|
||||||
|
LabeledContent("Refresh rate") {
|
||||||
|
TextField("Hz", value: $hz, format: .number.grouping(.never))
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
}
|
||||||
|
} else if refreshChoices.count > 1 {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("Refresh rate")
|
||||||
|
.font(.geist(15, relativeTo: .subheadline))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Picker("Refresh rate", selection: $hz) {
|
||||||
|
ForEach(refreshChoices, id: \.self) { rate in
|
||||||
|
Text("\(rate) Hz").tag(rate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// A device with a single supported rate (e.g. 60 Hz) has nothing to pick.
|
||||||
|
LabeledContent("Refresh rate") {
|
||||||
|
Text("\(hz) Hz").foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sentinel wheel tag for the "Custom…" row. Real tags are "WxH" (digits + "x"), so this can't
|
/// Sentinel wheel tag for the "Custom…" row. Real tags are "WxH" (digits + "x"), so this can't
|
||||||
/// collide with a resolution.
|
/// collide with a resolution.
|
||||||
private static let customResolutionTag = "custom"
|
private static let customResolutionTag = "custom"
|
||||||
@@ -156,6 +153,29 @@ extension SettingsView {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
/// The automatic-bitrate toggle + manual slider (and the >1 Gbps warning) rows.
|
||||||
|
@ViewBuilder private var bitrateRows: some View {
|
||||||
|
Toggle("Automatic bitrate", isOn: automaticBitrate)
|
||||||
|
if bitrateKbps != 0 {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Slider(value: bitrateSlider, in: 0...1) {
|
||||||
|
Text("Bitrate")
|
||||||
|
}
|
||||||
|
Text(SpeedTestSheet.mbpsLabel(kbps: bitrateKbps))
|
||||||
|
.monospacedDigit()
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(minWidth: 76, alignment: .trailing)
|
||||||
|
}
|
||||||
|
if bitrateKbps > 1_000_000 {
|
||||||
|
Label(Self.gigabitWarning, systemImage: "exclamationmark.triangle.fill")
|
||||||
|
.font(.geist(12, relativeTo: .caption))
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@ViewBuilder var audioSection: some View {
|
@ViewBuilder var audioSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Picker("Audio channels", selection: $audioChannels) {
|
Picker("Audio channels", selection: $audioChannels) {
|
||||||
|
|||||||
Reference in New Issue
Block a user