Files
Flaro/Halo/Views/Settings/PlaybackSettingsView.swift
T
ScarriffleandClaude Opus 5 4b9a1e7b67 Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit,
damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind.

712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25
Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit
`HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`,
`JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben.

`docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind
ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben
hiesse, die Vergangenheit zu glätten.

**Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis:
`com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte
Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container;
Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu
laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen.

**Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der
Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr
heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter
`…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur
anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim
Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor
dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet.

Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht
statt umbenannt — sie steht ohnehin nicht im Repo.

Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist
ein Handgriff ausserhalb des Projekts.

368 Tests grün. iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:12:32 +02:00

136 lines
5.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import HaloDesign
import HaloPlayback
/// Alles, was die Wiedergabe betrifft.
///
/// Vorher stand das verstreut in der langen Liste der Einstellungen, zwischen
/// Serverangaben und Downloads. Zusammen ergibt es einen Bildschirm, den man
/// einmal durchgeht und dann in Ruhe lässt.
struct PlaybackSettingsView: View {
@Environment(PlaybackSettings.self) private var settings
@Environment(\.haloAccent) private var accent
var body: some View {
@Bindable var settings = settings
Form {
SettingsHeader(
symbol: "play.rectangle",
colour: SettingsPalette.playback,
title: "settings.playback",
explanation: "settings.playbackExplanation"
)
Section {
Picker("settings.qualityWifi", selection: $settings.wifiQuality) {
ForEach(PlaybackSettings.Quality.allCases) { quality in
Text(quality.label).tag(quality)
}
}
Picker("settings.qualityCellular", selection: $settings.cellularQuality) {
ForEach(PlaybackSettings.Quality.allCases) { quality in
Text(quality.label).tag(quality)
}
}
} header: {
Text("settings.quality")
} footer: {
VStack(alignment: .leading, spacing: HaloSpacing.snug) {
Text("settings.qualityExplanation")
// **Nur mit Beleg.** Eine Warnung, die immer dasteht,
// liest nach dem zweiten Mal niemand mehr. Sie erscheint,
// wenn „ohne Grenze" eingestellt ist **und** die letzte
// Wiedergabe tatsächlich gehakt hat.
//
// Ohne Grenze kommt die Entscheidung oft bei direkter
// Wiedergabe heraus — und dann gibt es keine Stufe, auf
// die irgendetwas ausweichen könnte. Die Voreinstellung
// bleibt trotzdem, wie sie ist: zuhause am Kabel ist sie
// die richtige, und still umstellen wäre nicht in Ordnung.
if settings.wifiQuality == .original, settings.lastPlaybackStruggled {
Label {
Text("settings.qualityUnlimitedHint")
} icon: {
Image(systemName: "exclamationmark.triangle")
}
.font(.haloCaption)
.foregroundStyle(Color.haloTextSecondary)
}
}
}
Section {
Toggle("settings.rememberQuality", isOn: $settings.remembersQuality)
Toggle("settings.showBitrate", isOn: $settings.showsBitrate)
} footer: {
Text("settings.rememberQualityExplanation")
}
Section {
Picker("settings.rewind", selection: $settings.rewindSeconds) {
ForEach(PlaybackSettings.skipChoices, id: \.self) { seconds in
Text("settings.seconds \(seconds)").tag(seconds)
}
}
Picker("settings.forward", selection: $settings.forwardSeconds) {
ForEach(PlaybackSettings.skipChoices, id: \.self) { seconds in
Text("settings.seconds \(seconds)").tag(seconds)
}
}
Picker("settings.defaultRate", selection: $settings.defaultRate) {
ForEach(PlaybackSettings.rateChoices, id: \.self) { rate in
Text(rateLabel(rate)).tag(rate)
}
}
} header: {
Text("settings.controls")
} footer: {
// Zurück und vor getrennt, weil sie verschiedene Zwecke haben:
// zurück, weil man etwas nicht verstanden hat; vor, weil man
// etwas überspringen will.
Text("settings.controlsExplanation")
}
Section {
Toggle("settings.lockLandscape", isOn: $settings.locksLandscape)
} footer: {
Text("settings.lockLandscapeExplanation")
}
Section {
Toggle("settings.autoSkipIntro", isOn: $settings.autoSkipIntro)
Toggle("settings.autoSkipCredits", isOn: $settings.autoSkipCredits)
Toggle("settings.autoPlayNext", isOn: $settings.autoPlayNext)
} header: {
Text("settings.episodes")
}
}
.tint(accent)
.haloForm()
.scrollContentBackground(.hidden)
.background(Color.haloCanvas)
.navigationTitle(Text("settings.playback"))
.haloInlineTitle()
}
private func rateLabel(_ rate: Double) -> String {
rate == 1
? String(localized: "settings.rateNormal")
: rate.formatted(.number.precision(.fractionLength(0...2))) + "×"
}
}
extension PlaybackSettings.Quality {
var label: LocalizedStringKey {
switch self {
case .original: "quality.original"
case .high: "quality.high"
case .medium: "quality.medium"
case .low: "quality.low"
case .minimal: "quality.minimal"
}
}
}