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>
This commit is contained in:
Scarriffle
2026-10-03 11:12:32 +02:00
co-authored by Claude Opus 5
parent 2fcf6821c8
commit 4b9a1e7b67
280 changed files with 1456 additions and 1456 deletions
+3 -3
View File
@@ -9,8 +9,8 @@ xcuserdata/
# Von XcodeGen aus project.yml erzeugt — nicht eingecheckt, damit es keine # Von XcodeGen aus project.yml erzeugt — nicht eingecheckt, damit es keine
# Konflikte in der pbxproj gibt. Nach dem Klonen: xcodegen generate # Konflikte in der pbxproj gibt. Nach dem Klonen: xcodegen generate
# #
# Flixy.xcworkspace daneben ist dagegen von Hand geschrieben und gehört dazu: # Halo.xcworkspace daneben ist dagegen von Hand geschrieben und gehört dazu:
# er verweist nur auf das Projekt und überlebt jedes Neuerzeugen. Warum das # er verweist nur auf das Projekt und überlebt jedes Neuerzeugen. Warum das
# nötig ist, steht in Flixy.xcworkspace/README.md. # nötig ist, steht in Halo.xcworkspace/README.md.
Flixy.xcodeproj/ Halo.xcodeproj/
node_modules/ node_modules/
+2 -2
View File
@@ -1,8 +1,8 @@
import ActivityKit import ActivityKit
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
import FlixyDesign import HaloDesign
import FlixyDownloads import HaloDownloads
/// Der Download-Fortschritt im Sperrbildschirm und in der Dynamic Island. /// Der Download-Fortschritt im Sperrbildschirm und in der Dynamic Island.
struct DownloadLiveActivity: Widget { struct DownloadLiveActivity: Widget {
+1 -1
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Flixy Downloads</string> <string>Halo Downloads</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -1,9 +1,9 @@
# Diesen Arbeitsbereich öffnen, nicht das Projekt # Diesen Arbeitsbereich öffnen, nicht das Projekt
`Flixy.xcodeproj` wird von XcodeGen aus `project.yml` erzeugt — und dabei `Halo.xcodeproj` wird von XcodeGen aus `project.yml` erzeugt — und dabei
**ersetzt**, nicht bearbeitet: jede Datei darin bekommt einen neuen Inode, **ersetzt**, nicht bearbeitet: jede Datei darin bekommt einen neuen Inode,
auch die, deren Inhalt gleich bleibt. Hat Xcode den Arbeitsbereich **im** auch die, deren Inhalt gleich bleibt. Hat Xcode den Arbeitsbereich **im**
Projekt offen (`Flixy.xcodeproj/project.xcworkspace`), meldet es danach Projekt offen (`Halo.xcodeproj/project.xcworkspace`), meldet es danach
> The workspace file … has disappeared. Do you want to re-save the container, > The workspace file … has disappeared. Do you want to re-save the container,
> or close it? > or close it?
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Workspace version = "1.0"> <Workspace version = "1.0">
<FileRef location = "group:Flixy.xcodeproj"></FileRef> <FileRef location = "group:Halo.xcodeproj"></FileRef>
</Workspace> </Workspace>

Before

Width:  |  Height:  |  Size: 675 B

After

Width:  |  Height:  |  Size: 675 B

+4 -4
View File
@@ -9,21 +9,21 @@ import Foundation
/// ///
/// Der Name steht noch nicht fest. Damit ein späterer Wechsel nicht zur Suche /// Der Name steht noch nicht fest. Damit ein späterer Wechsel nicht zur Suche
/// quer durch den Quelltext wird, greift die App nirgends auf ein hart /// quer durch den Quelltext wird, greift die App nirgends auf ein hart
/// geschriebenes „Flixy" zu, sondern immer hierauf. Zu ändern sind dann genau /// geschriebenes „Halo" zu, sondern immer hierauf. Zu ändern sind dann genau
/// zwei Dinge: diese Datei und `project.yml` (Produktname, Bundle-Kennung, /// zwei Dinge: diese Datei und `project.yml` (Produktname, Bundle-Kennung,
/// Anzeigename). /// Anzeigename).
/// ///
/// Die Typen aus FlixyKit behalten ihre Namen — sie sind Bauteile, keine Marke. /// Die Typen aus HaloKit behalten ihre Namen — sie sind Bauteile, keine Marke.
enum Branding { enum Branding {
/// Wie die App sich selbst nennt. /// Wie die App sich selbst nennt.
static let appName = "Flixy" static let appName = "Halo"
/// Unter welchem Namen die Tokens im Schlüsselbund liegen. /// Unter welchem Namen die Tokens im Schlüsselbund liegen.
/// ///
/// An die Bundle-Kennung gebunden statt an den Anzeigenamen: eine spätere /// An die Bundle-Kennung gebunden statt an den Anzeigenamen: eine spätere
/// Umbenennung der App soll niemanden abmelden. /// Umbenennung der App soll niemanden abmelden.
static var keychainService: String { static var keychainService: String {
Bundle.main.bundleIdentifier ?? "com.scarriffleservices.flixy" Bundle.main.bundleIdentifier ?? "com.scarriffleservices.halo"
} }
/// Wann dieser Build entstanden ist. /// Wann dieser Build entstanden ist.
+10 -10
View File
@@ -3,14 +3,14 @@ import SwiftUI
#if os(iOS) #if os(iOS)
import UIKit import UIKit
#endif #endif
import FlixyDesign import HaloDesign
import FlixyDownloads import HaloDownloads
import FlixyPlayback import HaloPlayback
import JellyfinKit import JellyfinKit
import MediaKit import MediaKit
@main @main
struct FlixyApp: App { struct HaloApp: App {
#if os(iOS) #if os(iOS)
/// Nimmt das Signal entgegen, mit dem iOS die App zum Abschluss von /// Nimmt das Signal entgegen, mit dem iOS die App zum Abschluss von
/// Hintergrund-Downloads aufweckt. Dafür gibt es in SwiftUI keinen /// Hintergrund-Downloads aufweckt. Dafür gibt es in SwiftUI keinen
@@ -23,8 +23,8 @@ struct FlixyApp: App {
/// Leben hier, nicht in einer View: sie gelten für die gesamte App, und /// Leben hier, nicht in einer View: sie gelten für die gesamte App, und
/// ein Neuaufbau der Wurzelansicht darf weder die Anmeldung noch laufende /// ein Neuaufbau der Wurzelansicht darf weder die Anmeldung noch laufende
/// Downloads zurücksetzen. /// Downloads zurücksetzen.
@State private var theme = FlixyTheme() @State private var theme = HaloTheme()
@State private var session: FlixySession @State private var session: HaloSession
@State private var playback = PlaybackCoordinator() @State private var playback = PlaybackCoordinator()
@State private var playbackSettings = PlaybackSettings() @State private var playbackSettings = PlaybackSettings()
@State private var languages = LanguagePreferences() @State private var languages = LanguagePreferences()
@@ -40,7 +40,7 @@ struct FlixyApp: App {
private let modelContainer: ModelContainer private let modelContainer: ModelContainer
init() { init() {
_session = State(initialValue: FlixySession( _session = State(initialValue: HaloSession(
jellyfinIdentity: JellyfinIdentity.current( jellyfinIdentity: JellyfinIdentity.current(
client: Branding.appName, client: Branding.appName,
version: Branding.version version: Branding.version
@@ -74,8 +74,8 @@ struct FlixyApp: App {
// Notizzettelgröße, und darin fällt der Aufmacher in sich // Notizzettelgröße, und darin fällt der Aufmacher in sich
// zusammen, weil seine Abstände dehnbar sind. // zusammen, weil seine Abstände dehnbar sind.
.frame( .frame(
minWidth: FlixyPlatform.isMac ? 900 : nil, minWidth: HaloPlatform.isMac ? 900 : nil,
minHeight: FlixyPlatform.isMac ? 620 : nil minHeight: HaloPlatform.isMac ? 620 : nil
) )
// `defaultSize` gibt es hier nicht; die Größe hängt an der // `defaultSize` gibt es hier nicht; die Größe hängt an der
// Szene. // Szene.
@@ -133,7 +133,7 @@ struct FlixyApp: App {
} }
#if os(iOS) #if os(iOS)
extension FlixyApp { extension HaloApp {
/// Gibt dem Fenster eine vernünftige Mindestgröße. /// Gibt dem Fenster eine vernünftige Mindestgröße.
/// ///
/// Auf dem Mac öffnet das System sonst ein Fenster von der Größe eines /// Auf dem Mac öffnet das System sonst ein Fenster von der Größe eines
+2 -2
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Flixy</string> <string>Halo</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -28,7 +28,7 @@
<true/> <true/>
</dict> </dict>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Flixy benötigt Zugriff auf das lokale Netzwerk, um deinen Jellyfin-Server zu erreichen und Medien direkt von ihm zu laden.</string> <string>Halo benötigt Zugriff auf das lokale Netzwerk, um deinen Jellyfin-Server zu erreichen und Medien direkt von ihm zu laden.</string>
<key>NSSupportsLiveActivities</key> <key>NSSupportsLiveActivities</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
@@ -5647,13 +5647,13 @@
"de": { "de": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "Die Obergrenze, nicht das Ziel. Im eigenen Netz überträgt der Server das Original; unterwegs beginnt Flixy kleiner und tastet sich hinauf, solange die Verbindung es trägt — höchstens bis zu dieser Stufe. Auflösung und Bitrate gehen dabei zusammen." "value": "Die Obergrenze, nicht das Ziel. Im eigenen Netz überträgt der Server das Original; unterwegs beginnt Halo kleiner und tastet sich hinauf, solange die Verbindung es trägt — höchstens bis zu dieser Stufe. Auflösung und Bitrate gehen dabei zusammen."
} }
}, },
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "An upper limit, not a target. On your own network the server sends the original; away from home Flixy starts lower and works its way up as long as the connection holds — never above this step. Resolution and bitrate move together." "value": "An upper limit, not a target. On your own network the server sends the original; away from home Halo starts lower and works its way up as long as the connection holds — never above this step. Resolution and bitrate move together."
} }
} }
} }
@@ -1,5 +1,5 @@
import Foundation import Foundation
import FlixyDownloads import HaloDownloads
import MediaKit import MediaKit
/// Füllt den Vorrat an ungesehenen Folgen wieder auf. /// Füllt den Vorrat an ungesehenen Folgen wieder auf.
@@ -1,6 +1,6 @@
import Foundation import Foundation
import FlixyDownloads import HaloDownloads
import FlixyPlayback import HaloPlayback
import MediaKit import MediaKit
/// Übersetzt Titel vom Server in Download-Aufträge. /// Übersetzt Titel vom Server in Download-Aufträge.
@@ -15,7 +15,7 @@ import Observation
/// Umschalten ja, nebeneinander nein. /// Umschalten ja, nebeneinander nein.
@MainActor @MainActor
@Observable @Observable
final class FlixySession { final class HaloSession {
enum Phase: Equatable { enum Phase: Equatable {
/// Beim Start: liegt ein Token im Schlüsselbund? /// Beim Start: liegt ein Token im Schlüsselbund?
case launching case launching
@@ -32,11 +32,11 @@ final class FlixySession {
} }
private enum Key { private enum Key {
static let jellyfinAddress = "flixy.jellyfin.address" static let jellyfinAddress = "halo.jellyfin.address"
static let jellyfinUserID = "flixy.jellyfin.userID" static let jellyfinUserID = "halo.jellyfin.userID"
static let jellyfinUserName = "flixy.jellyfin.userName" static let jellyfinUserName = "halo.jellyfin.userName"
static let jellyfinServerName = "flixy.jellyfin.serverName" static let jellyfinServerName = "halo.jellyfin.serverName"
static let jellyfinIsAdministrator = "flixy.jellyfin.isAdministrator" static let jellyfinIsAdministrator = "halo.jellyfin.isAdministrator"
} }
/// Unter welchem Namen das Server-Token im Schlüsselbund liegt. /// Unter welchem Namen das Server-Token im Schlüsselbund liegt.
@@ -55,14 +55,14 @@ final class FlixySession {
/// könnte. Deshalb wird einmalig aufgeräumt, bevor die Namen endgültig /// könnte. Deshalb wird einmalig aufgeräumt, bevor die Namen endgültig
/// verschwinden. /// verschwinden.
private enum Altlast { private enum Altlast {
static let merker = "flixy.migration.plexPurged" static let merker = "halo.migration.plexPurged"
static let tokenKonten = ["plex.account", "plex.server"] static let tokenKonten = ["plex.account", "plex.server"]
static let schlüssel = [ static let schlüssel = [
"flixy.backend.kind", "halo.backend.kind",
"flixy.server.clientIdentifier", "halo.server.clientIdentifier",
"flixy.server.connectionURI", "halo.server.connectionURI",
"flixy.server.name", "halo.server.name",
"flixy.server.connectionIsLocal", "halo.server.connectionIsLocal",
] ]
} }
@@ -93,7 +93,7 @@ final class FlixySession {
/// Wer angemeldet ist — sofern der Server einen Namen dafür kennt. /// Wer angemeldet ist — sofern der Server einen Namen dafür kennt.
var accountName: String? { jellyfinAccount?.userName } var accountName: String? { jellyfinAccount?.userName }
/// Die Mediatheken, die Flixy darstellen kann — Musik und Fotos /// Die Mediatheken, die Halo darstellen kann — Musik und Fotos
/// erscheinen gar nicht erst. /// erscheinen gar nicht erst.
private(set) var libraries: [MediaLibrary] = [] private(set) var libraries: [MediaLibrary] = []
@@ -233,14 +233,14 @@ final class FlixySession {
/// Meldet sich an der Attrappe aus `Tools/JellyfinStub` an. /// Meldet sich an der Attrappe aus `Tools/JellyfinStub` an.
/// ///
/// Nur in der Entwicklung und nur auf ausdrückliches Kommando: /// Nur in der Entwicklung und nur auf ausdrückliches Kommando:
/// `-FlixyStubServer http://127.0.0.1:8096` als Startargument. Der Weg /// `-HaloStubServer http://127.0.0.1:8096` als Startargument. Der Weg
/// führt bewusst **am Schlüsselbund vorbei** und schreibt nichts fort — /// führt bewusst **am Schlüsselbund vorbei** und schreibt nichts fort —
/// so lässt sich die Oberfläche ansehen, prüfen und fotografieren, ohne /// so lässt sich die Oberfläche ansehen, prüfen und fotografieren, ohne
/// dass je ein echter Server, ein echtes Konto oder ein echter Token im /// dass je ein echter Server, ein echtes Konto oder ein echter Token im
/// Spiel wäre. Genau daran hat es bisher gefehlt: Aussagen über das /// Spiel wäre. Genau daran hat es bisher gefehlt: Aussagen über das
/// Aussehen waren Vermutungen, weil niemand hinsehen konnte. /// Aussehen waren Vermutungen, weil niemand hinsehen konnte.
private func adoptStubServerIfRequested() async -> Bool { private func adoptStubServerIfRequested() async -> Bool {
guard let raw = defaults.string(forKey: "FlixyStubServer"), guard let raw = defaults.string(forKey: "HaloStubServer"),
let baseURL = URL(string: raw) let baseURL = URL(string: raw)
else { return false } else { return false }
@@ -1,5 +1,5 @@
import Foundation import Foundation
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Woher der Aufmacher seine Bilder nimmt. /// Woher der Aufmacher seine Bilder nimmt.
@@ -64,8 +64,8 @@ enum HeroArtwork {
} }
static func aspectRatio(_ placement: Placement = .carousel, compact: Bool) -> CGFloat { static func aspectRatio(_ placement: Placement = .carousel, compact: Bool) -> CGFloat {
if isPortrait(placement, compact: compact) { return FlixyAspectRatio.poster } if isPortrait(placement, compact: compact) { return HaloAspectRatio.poster }
return compact ? FlixyAspectRatio.heroCompact : FlixyAspectRatio.hero return compact ? HaloAspectRatio.heroCompact : HaloAspectRatio.hero
} }
static func backdropURL( static func backdropURL(
@@ -12,15 +12,15 @@ import MediaKit
@Observable @Observable
final class HomeLayoutSettings { final class HomeLayoutSettings {
private enum Key { private enum Key {
static let hidden = "flixy.home.hiddenHubs" static let hidden = "halo.home.hiddenHubs"
static let order = "flixy.home.hubOrder" static let order = "halo.home.hubOrder"
static let heroCount = "flixy.home.heroCount" static let heroCount = "halo.home.heroCount"
static let heroSource = "flixy.home.heroSource" static let heroSource = "halo.home.heroSource"
static let heroLibraries = "flixy.home.heroLibraries" static let heroLibraries = "halo.home.heroLibraries"
static let combinesContinueRows = "flixy.home.combinesContinueRows" static let combinesContinueRows = "halo.home.combinesContinueRows"
static let hidesWatchedInLatest = "flixy.home.hidesWatchedInLatest" static let hidesWatchedInLatest = "halo.home.hidesWatchedInLatest"
static let cardScale = "flixy.home.cardScale" static let cardScale = "halo.home.cardScale"
static let heroHeight = "flixy.home.heroHeight" static let heroHeight = "halo.home.heroHeight"
} }
/// Woraus sich der Aufmacher speist. /// Woraus sich der Aufmacher speist.
@@ -5,14 +5,14 @@ import Observation
/// Führt durch die Anmeldung an einem Jellyfin-Server. /// Führt durch die Anmeldung an einem Jellyfin-Server.
/// ///
/// Eigenes Modell statt eines weiteren Falls in `FlixySession.Phase`: die /// Eigenes Modell statt eines weiteren Falls in `HaloSession.Phase`: die
/// Anmeldung besteht aus mehreren Schritten mit eigener Bedienung — Adresse /// Anmeldung besteht aus mehreren Schritten mit eigener Bedienung — Adresse
/// eintippen, Code bestätigen lassen, notfalls doch Benutzername und Passwort /// eintippen, Code bestätigen lassen, notfalls doch Benutzername und Passwort
/// —, und keiner davon interessiert den Rest der App. Was hier ankommt, ist am /// —, und keiner davon interessiert den Rest der App. Was hier ankommt, ist am
/// Ende ein einziges Ergebnis: ein Konto und ein Weg dorthin. /// Ende ein einziges Ergebnis: ein Konto und ein Weg dorthin.
/// ///
/// **Quick Connect zuerst.** Der sechsstellige Code wird in einem bereits /// **Quick Connect zuerst.** Der sechsstellige Code wird in einem bereits
/// angemeldeten Client bestätigt; das Passwort erreicht Flixy dabei nie — /// angemeldeten Client bestätigt; das Passwort erreicht Halo dabei nie —
/// dieselbe Zusage wie bei Plex, wo die Anmeldemaske von Plex selbst kommt. /// dieselbe Zusage wie bei Plex, wo die Anmeldemaske von Plex selbst kommt.
/// Benutzername und Passwort sind der Rückfall für Server, auf denen Quick /// Benutzername und Passwort sind der Rückfall für Server, auf denen Quick
/// Connect abgeschaltet ist. Dann hält die App das Passwort für die Dauer /// Connect abgeschaltet ist. Dann hält die App das Passwort für die Dauer
@@ -1,7 +1,7 @@
import Foundation import Foundation
import Observation import Observation
import MediaKit import MediaKit
import FlixyPlayback import HaloPlayback
/// Welche Tonspur gewählt werden soll — und auf welcher Ebene das jemand /// Welche Tonspur gewählt werden soll — und auf welcher Ebene das jemand
/// gesagt hat. /// gesagt hat.
@@ -28,13 +28,13 @@ final class LanguagePreferences {
private enum Key { private enum Key {
/// Absichtlich die Schlüssel aus `PlaybackSettings`: hier ist nur der /// Absichtlich die Schlüssel aus `PlaybackSettings`: hier ist nur der
/// Besitzer gewechselt, nicht der Wert. /// Besitzer gewechselt, nicht der Wert.
static let audio = "flixy.playback.audioLanguage" static let audio = "halo.playback.audioLanguage"
static let subtitles = "flixy.playback.subtitleLanguage" static let subtitles = "halo.playback.subtitleLanguage"
static let fallbacks = "flixy.languages.fallbacks" static let fallbacks = "halo.languages.fallbacks"
static let avoidsAudioDescription = "flixy.languages.avoidsAudioDescription" static let avoidsAudioDescription = "halo.languages.avoidsAudioDescription"
static let perLibrary = "flixy.languages.library" static let perLibrary = "halo.languages.library"
static let perSeries = "flixy.languages.series" static let perSeries = "halo.languages.series"
static let rememberedTracks = "flixy.languages.seriesTrack" static let rememberedTracks = "halo.languages.seriesTrack"
} }
/// Die Sprachen, die zur Auswahl stehen. /// Die Sprachen, die zur Auswahl stehen.
@@ -10,7 +10,7 @@ import MediaKit
/// sieht nicht nach einem Fehler aus, sondern nach einem Server ohne Bild. /// sieht nicht nach einem Fehler aus, sondern nach einem Server ohne Bild.
/// ///
/// Reine Ableitungen aus `MediaItem`, keine Ansicht — deshalb neben den /// Reine Ableitungen aus `MediaItem`, keine Ansicht — deshalb neben den
/// übrigen Modellen und nicht in FlixyDesign, das `MediaItem` gar nicht kennt. /// übrigen Modellen und nicht in HaloDesign, das `MediaItem` gar nicht kennt.
extension MediaItem { extension MediaItem {
/// Der Titel auf einer Kachel in einer Reihe. /// Der Titel auf einer Kachel in einer Reihe.
/// ///
@@ -13,8 +13,8 @@ import MediaKit
@Observable @Observable
final class NavigationLayoutSettings { final class NavigationLayoutSettings {
private enum Key { private enum Key {
static let hidden = "flixy.navigation.hidden" static let hidden = "halo.navigation.hidden"
static let order = "flixy.navigation.order" static let order = "halo.navigation.order"
} }
private let defaults: UserDefaults private let defaults: UserDefaults
@@ -1,6 +1,6 @@
import Foundation import Foundation
import Observation import Observation
import FlixyPlayback import HaloPlayback
/// Hält fest, was beim letzten Abspielversuch passiert ist. /// Hält fest, was beim letzten Abspielversuch passiert ist.
/// ///
@@ -1,6 +1,6 @@
import Foundation import Foundation
import Observation import Observation
import FlixyPlayback import HaloPlayback
/// Was der Nutzer über die Wiedergabe eingestellt hat. /// Was der Nutzer über die Wiedergabe eingestellt hat.
/// ///
@@ -28,19 +28,19 @@ final class PlaybackSettings {
} }
private enum Key { private enum Key {
static let wifiQuality = "flixy.playback.quality.wifi" static let wifiQuality = "halo.playback.quality.wifi"
static let cellularQuality = "flixy.playback.quality.cellular" static let cellularQuality = "halo.playback.quality.cellular"
static let autoSkipIntro = "flixy.playback.autoSkipIntro" static let autoSkipIntro = "halo.playback.autoSkipIntro"
static let autoSkipCredits = "flixy.playback.autoSkipCredits" static let autoSkipCredits = "halo.playback.autoSkipCredits"
static let autoPlayNext = "flixy.playback.autoPlayNext" static let autoPlayNext = "halo.playback.autoPlayNext"
static let locksLandscape = "flixy.playback.locksLandscape" static let locksLandscape = "halo.playback.locksLandscape"
static let rewindSeconds = "flixy.playback.rewindSeconds" static let rewindSeconds = "halo.playback.rewindSeconds"
static let forwardSeconds = "flixy.playback.forwardSeconds" static let forwardSeconds = "halo.playback.forwardSeconds"
static let defaultRate = "flixy.playback.defaultRate" static let defaultRate = "halo.playback.defaultRate"
static let remembersQuality = "flixy.playback.remembersQuality" static let remembersQuality = "halo.playback.remembersQuality"
static let rememberedQuality = "flixy.playback.rememberedQuality" static let rememberedQuality = "halo.playback.rememberedQuality"
static let showsBitrate = "flixy.playback.showsBitrate" static let showsBitrate = "halo.playback.showsBitrate"
static let lastPlaybackStruggled = "flixy.playback.struggled" static let lastPlaybackStruggled = "halo.playback.struggled"
} }
/// Die Sprünge, die es zur Auswahl gibt. /// Die Sprünge, die es zur Auswahl gibt.
@@ -15,7 +15,7 @@ import Observation
final class SearchFeed { final class SearchFeed {
/// Wie viele zuletzt gesuchte Begriffe gemerkt werden. /// Wie viele zuletzt gesuchte Begriffe gemerkt werden.
private static let recentLimit = 8 private static let recentLimit = 8
private static let recentKey = "flixy.search.recent" private static let recentKey = "halo.search.recent"
private(set) var hubs: [MediaRowGroup] = [] private(set) var hubs: [MediaRowGroup] = []
private(set) var isSearching = false private(set) var isSearching = false
@@ -1,6 +1,6 @@
import Foundation import Foundation
import Observation import Observation
import FlixyPlayback import HaloPlayback
/// Wie Untertitel aussehen sollen — gemerkt über Neustarts hinweg. /// Wie Untertitel aussehen sollen — gemerkt über Neustarts hinweg.
/// ///
@@ -14,8 +14,8 @@ import FlixyPlayback
@Observable @Observable
final class SubtitleAppearance { final class SubtitleAppearance {
private enum Key { private enum Key {
static let size = "flixy.subtitles.size" static let size = "halo.subtitles.size"
static let palette = "flixy.subtitles.palette" static let palette = "halo.subtitles.palette"
} }
private let defaults: UserDefaults private let defaults: UserDefaults
@@ -1,4 +1,4 @@
import FlixyPlayback import HaloPlayback
import Foundation import Foundation
/// Die Namen der Stufen stehen neben dem Modell, nicht darin — dasselbe /// Die Namen der Stufen stehen neben dem Modell, nicht darin — dasselbe
@@ -1,6 +1,6 @@
// **Die ganze Datei nur für den Fernseher.** // **Die ganze Datei nur für den Fernseher.**
// //
// Sie liegt im geteilten Ordner, weil `Flixy/Session/` von beiden Zielen // Sie liegt im geteilten Ordner, weil `Halo/Session/` von beiden Zielen
// übersetzt wird — und `TVServices` gibt es auf iOS nicht. Ein `#if` um den // übersetzt wird — und `TVServices` gibt es auf iOS nicht. Ein `#if` um den
// einen Import genügte dabei nicht: der Abhängigkeits-Sucher von Clang prüft // einen Import genügte dabei nicht: der Abhängigkeits-Sucher von Clang prüft
// ihn trotzdem und bricht mit „Unable to resolve module dependency: // ihn trotzdem und bricht mit „Unable to resolve module dependency:
@@ -21,7 +21,7 @@ import TVServices
@MainActor @MainActor
enum TopShelfWriter { enum TopShelfWriter {
/// Die App-Gruppe, die App und Erweiterung teilen. /// Die App-Gruppe, die App und Erweiterung teilen.
static let group = "group.com.scarriffleservices.flixy" static let group = "group.com.scarriffleservices.halo"
/// Die Kante, in der das Regal Plakate zeigt. /// Die Kante, in der das Regal Plakate zeigt.
/// ///
@@ -99,5 +99,5 @@ extension EnvironmentValues {
/// ///
/// Die Voreinstellung tut nichts: wo niemand etwas hinterlegt, gibt es /// Die Voreinstellung tut nichts: wo niemand etwas hinterlegt, gibt es
/// auch nichts neu zu laden. /// auch nichts neu zu laden.
@Entry var flixyItemChanged: () async -> Void = {} @Entry var haloItemChanged: () async -> Void = {}
} }
@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyDownloads import HaloDownloads
import MediaKit import MediaKit
/// Eine Kachel, die zur Detailseite führt — mit einem Knopf, der sofort /// Eine Kachel, die zur Detailseite führt — mit einem Knopf, der sofort
@@ -11,7 +11,7 @@ import MediaKit
/// Zwei Schritte für eine Absicht. /// Zwei Schritte für eine Absicht.
/// ///
/// **Warum eine Hülle und keine Änderung an der Karte.** `PosterCard` liegt /// **Warum eine Hülle und keine Änderung an der Karte.** `PosterCard` liegt
/// in `FlixyDesign`, und das Paket ist ohne Abhängigkeiten deklariert — es /// in `HaloDesign`, und das Paket ist ohne Abhängigkeiten deklariert — es
/// kennt weder `MediaItem` noch `PlaybackCoordinator`. Ein Überzug **in** der /// kennt weder `MediaItem` noch `PlaybackCoordinator`. Ein Überzug **in** der
/// Karte fiele ausserdem unter ihr `.accessibilityElement(children: .combine)` /// Karte fiele ausserdem unter ihr `.accessibilityElement(children: .combine)`
/// und wäre für VoiceOver gar nicht mehr da. /// und wäre für VoiceOver gar nicht mehr da.
@@ -27,12 +27,12 @@ struct QuickPlayTile<Card: View>: View {
var opensDetails: Bool = false var opensDetails: Bool = false
@ViewBuilder let card: () -> Card @ViewBuilder let card: () -> Card
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(PlaybackCoordinator.self) private var playback @Environment(PlaybackCoordinator.self) private var playback
@Environment(QuickPlay.self) private var quickPlay @Environment(QuickPlay.self) private var quickPlay
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(LanguagePreferences.self) private var languages @Environment(LanguagePreferences.self) private var languages
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var isPointerInside = false @State private var isPointerInside = false
@State private var errorMessage: String? @State private var errorMessage: String?
@@ -48,11 +48,11 @@ struct QuickPlayTile<Card: View>: View {
/// Was die umgebende Seite tun soll, wenn sich am Server etwas geändert /// Was die umgebende Seite tun soll, wenn sich am Server etwas geändert
/// hat. Siehe `ItemActionEntries`. /// hat. Siehe `ItemActionEntries`.
@Environment(\.flixyItemChanged) private var itemChanged @Environment(\.haloItemChanged) private var itemChanged
/// Der Weg auf die Detailseite — siehe `MediaStack`. /// Der Weg auf die Detailseite — siehe `MediaStack`.
@Environment(\.flixyPush) private var push @Environment(\.haloPush) private var push
private var isMac: Bool { FlixyPlatform.isMac } private var isMac: Bool { HaloPlatform.isMac }
/// Ob es überhaupt etwas zu starten gibt. /// Ob es überhaupt etwas zu starten gibt.
/// ///
@@ -236,7 +236,7 @@ struct QuickPlayTile<Card: View>: View {
.shadow(color: .black.opacity(0.35), radius: 3, y: 1) .shadow(color: .black.opacity(0.35), radius: 3, y: 1)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
.opacity(isPointerInside ? 1 : 0) .opacity(isPointerInside ? 1 : 0)
// Er ist eine Zeigergeste; für VoiceOver gibt es die benannte Aktion // Er ist eine Zeigergeste; für VoiceOver gibt es die benannte Aktion
// oben. Beides anzubieten hiesse, jede Kachel zu verdoppeln. // oben. Beides anzubieten hiesse, jede Kachel zu verdoppeln.
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Das Aktionsmenü auf einer Detailseite. /// Das Aktionsmenü auf einer Detailseite.
@@ -17,7 +17,7 @@ struct ItemActionsMenu: View {
/// Detailseite neu geladen werden sollte. /// Detailseite neu geladen werden sollte.
let onChange: () async -> Void let onChange: () async -> Void
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@State private var isWorking = false @State private var isWorking = false
@State private var errorMessage: String? @State private var errorMessage: String?
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Die Detailseite eines Titels. /// Die Detailseite eines Titels.
@@ -10,7 +10,7 @@ import MediaKit
struct MediaDetailView: View { struct MediaDetailView: View {
let route: MediaRoute let route: MediaRoute
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@State private var state: LoadState<MediaItem> = .loading @State private var state: LoadState<MediaItem> = .loading
var body: some View { var body: some View {
@@ -37,12 +37,12 @@ struct MediaDetailView: View {
} }
} }
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(state.value?.title ?? route.title) .navigationTitle(state.value?.title ?? route.title)
.flixyInlineTitle() .haloInlineTitle()
.toolbar { .toolbar {
if let item = state.value { if let item = state.value {
ToolbarItem(placement: .flixyTrailing) { ToolbarItem(placement: .haloTrailing) {
ItemActionsMenu(item: item) { await load() } ItemActionsMenu(item: item) { await load() }
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Der Weg nach oben: von der Folge zur Staffel, von der Staffel zur Serie. /// Der Weg nach oben: von der Folge zur Staffel, von der Staffel zur Serie.
@@ -15,7 +15,7 @@ struct ParentLinks: View {
var body: some View { var body: some View {
if !destinations.isEmpty { if !destinations.isEmpty {
ScrollView(.horizontal) { ScrollView(.horizontal) {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
ForEach(destinations, id: \.route.ratingKey) { destination in ForEach(destinations, id: \.route.ratingKey) { destination in
NavigationLink(value: destination.route) { NavigationLink(value: destination.route) {
Label { Label {
@@ -24,16 +24,16 @@ struct ParentLinks: View {
} icon: { } icon: {
Image(systemName: destination.symbol) Image(systemName: destination.symbol)
} }
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.background(Color.flixyElevated, in: .capsule) .background(Color.haloElevated, in: .capsule)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
.scrollIndicators(.never) .scrollIndicators(.never)
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Die Detailseite von etwas, das sich direkt abspielen lässt — Film oder /// Die Detailseite von etwas, das sich direkt abspielen lässt — Film oder
@@ -7,9 +7,9 @@ import MediaKit
struct PlayableDetailView: View { struct PlayableDetailView: View {
let item: MediaItem let item: MediaItem
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(PlaybackCoordinator.self) private var playback @Environment(PlaybackCoordinator.self) private var playback
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.horizontalSizeClass) private var sizeClass
@State private var selectedVersion = 0 @State private var selectedVersion = 0
@@ -17,7 +17,7 @@ struct PlayableDetailView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: FlixySpacing.section) { VStack(alignment: .leading, spacing: HaloSpacing.section) {
header header
// Bei einer Folge nicht mehr: dorthin führt jetzt die Zeile // Bei einer Folge nicht mehr: dorthin führt jetzt die Zeile
@@ -29,14 +29,14 @@ struct PlayableDetailView: View {
if item.hasMultipleVersions { if item.hasMultipleVersions {
VersionPicker(media: item.versions, selection: $selectedVersion) VersionPicker(media: item.versions, selection: $selectedVersion)
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
if let summary = item.summary, !summary.isEmpty { if let summary = item.summary, !summary.isEmpty {
Text(summary) Text(summary)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
if !item.genres.isEmpty { if !item.genres.isEmpty {
@@ -53,13 +53,13 @@ struct PlayableDetailView: View {
part: part, part: part,
isExpanded: $showsTechnicalDetails isExpanded: $showsTechnicalDetails
) )
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
} }
.padding(.bottom, FlixySpacing.section) .padding(.bottom, HaloSpacing.section)
} }
.scrollIndicators(.never) .scrollIndicators(.never)
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
private var currentMedia: MediaVersion? { private var currentMedia: MediaVersion? {
@@ -104,9 +104,9 @@ struct PlayableDetailView: View {
item.watchProgress == nil ? "hero.play" : "hero.resume", item.watchProgress == nil ? "hero.play" : "hero.resume",
systemImage: "play.fill" systemImage: "play.fill"
) )
.font(.flixyCardTitle) .font(.haloCardTitle)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
@@ -118,11 +118,11 @@ struct PlayableDetailView: View {
} label: { } label: {
Label("detail.playFromStart", systemImage: "gobackward") Label("detail.playFromStart", systemImage: "gobackward")
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.flixyCardTitle) .font(.haloCardTitle)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
.tint(Color.flixyTextSecondary) .tint(Color.haloTextSecondary)
} }
DownloadButton(item: item, mediaIndex: selectedVersion) DownloadButton(item: item, mediaIndex: selectedVersion)
@@ -210,10 +210,10 @@ struct VersionPicker: View {
@Binding var selection: Int @Binding var selection: Int
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.snug) { VStack(alignment: .leading, spacing: HaloSpacing.snug) {
Text("detail.versions") Text("detail.versions")
.font(.flixyRowHeader) .font(.haloRowHeader)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Picker("detail.versions", selection: $selection) { Picker("detail.versions", selection: $selection) {
ForEach(Array(media.enumerated()), id: \.offset) { index, version in ForEach(Array(media.enumerated()), id: \.offset) { index, version in
@@ -232,17 +232,17 @@ struct TagStrip: View {
var body: some View { var body: some View {
ScrollView(.horizontal) { ScrollView(.horizontal) {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
ForEach(tags, id: \.self) { tag in ForEach(tags, id: \.self) { tag in
Text(tag) Text(tag)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.vertical, FlixySpacing.tight) .padding(.vertical, HaloSpacing.tight)
.background(Color.flixyElevated, in: .capsule) .background(Color.haloElevated, in: .capsule)
} }
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
.scrollIndicators(.never) .scrollIndicators(.never)
} }
@@ -251,20 +251,20 @@ struct TagStrip: View {
/// Die Besetzung mit Porträts. /// Die Besetzung mit Porträts.
struct CastRow: View { struct CastRow: View {
let cast: [MediaPerson] let cast: [MediaPerson]
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
Text("detail.cast") Text("detail.cast")
.font(.flixyRowHeader) .font(.haloRowHeader)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
ScrollView(.horizontal) { ScrollView(.horizontal) {
HStack(alignment: .top, spacing: FlixySpacing.regular) { HStack(alignment: .top, spacing: HaloSpacing.regular) {
ForEach(cast.prefix(20)) { role in ForEach(cast.prefix(20)) { role in
VStack(spacing: FlixySpacing.snug) { VStack(spacing: HaloSpacing.snug) {
FlixyArtwork( HaloArtwork(
url: session.backend?.artworkURL(path: role.thumb, width: 160, height: 160), url: session.backend?.artworkURL(path: role.thumb, width: 160, height: 160),
aspectRatio: 1, aspectRatio: 1,
fallbackSymbol: "person" fallbackSymbol: "person"
@@ -273,15 +273,15 @@ struct CastRow: View {
.clipShape(.circle) .clipShape(.circle)
Text(role.name) Text(role.name)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
if let character = role.role { if let character = role.role {
Text(character) Text(character)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.lineLimit(1) .lineLimit(1)
} }
} }
@@ -289,7 +289,7 @@ struct CastRow: View {
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
} }
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
.scrollIndicators(.never) .scrollIndicators(.never)
} }
@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyDownloads import HaloDownloads
import MediaKit import MediaKit
/// Die Detailseite einer Serie oder Staffel. /// Die Detailseite einer Serie oder Staffel.
@@ -10,10 +10,10 @@ import MediaKit
struct SeriesDetailView: View { struct SeriesDetailView: View {
let item: MediaItem let item: MediaItem
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(LanguagePreferences.self) private var languages @Environment(LanguagePreferences.self) private var languages
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.horizontalSizeClass) private var sizeClass
@State private var children: LoadState<[MediaItem]> = .loading @State private var children: LoadState<[MediaItem]> = .loading
@@ -35,16 +35,16 @@ struct SeriesDetailView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: FlixySpacing.section) { VStack(alignment: .leading, spacing: HaloSpacing.section) {
header header
ParentLinks(item: item) ParentLinks(item: item)
if let summary = item.summary, !summary.isEmpty { if let summary = item.summary, !summary.isEmpty {
Text(summary) Text(summary)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
if !item.genres.isEmpty { TagStrip(tags: item.genres) } if !item.genres.isEmpty { TagStrip(tags: item.genres) }
@@ -58,17 +58,17 @@ struct SeriesDetailView: View {
scope: .series(item.id), scope: .series(item.id),
title: "languages.forThisSeries" title: "languages.forThisSeries"
) )
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
childList childList
if !item.cast.isEmpty { CastRow(cast: item.cast) } if !item.cast.isEmpty { CastRow(cast: item.cast) }
} }
.padding(.bottom, FlixySpacing.section) .padding(.bottom, HaloSpacing.section)
} }
.scrollIndicators(.never) .scrollIndicators(.never)
.flixyRevealsTopChrome() .haloRevealsTopChrome()
.safeAreaInset(edge: .bottom, spacing: 0) { selectionBar } .safeAreaInset(edge: .bottom, spacing: 0) { selectionBar }
.confirmationDialog( .confirmationDialog(
Text("downloads.quality.title"), Text("downloads.quality.title"),
@@ -163,34 +163,34 @@ struct SeriesDetailView: View {
private var childList: some View { private var childList: some View {
switch children { switch children {
case .loading: case .loading:
ProgressView().frame(maxWidth: .infinity).padding(.vertical, FlixySpacing.section) ProgressView().frame(maxWidth: .infinity).padding(.vertical, HaloSpacing.section)
case .failed(let message): case .failed(let message):
Text(message) Text(message)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
case .loaded(let entries): case .loaded(let entries):
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
// **Der Auswählen-Knopf steht bei dem, was er auswählt.** // **Der Auswählen-Knopf steht bei dem, was er auswählt.**
// Oben in der Werkzeugleiste wäre er weiter weg von der Liste // Oben in der Werkzeugleiste wäre er weiter weg von der Liste
// als von allem anderen auf dieser Seite. // als von allem anderen auf dieser Seite.
HStack { HStack {
Text(item.kind == .show ? "detail.seasons" : "detail.episodes") Text(item.kind == .show ? "detail.seasons" : "detail.episodes")
.font(.flixyRowHeader) .font(.haloRowHeader)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Spacer() Spacer()
if entries.contains(where: { $0.kind == .episode }) { if entries.contains(where: { $0.kind == .episode }) {
selectionControls(for: entries) selectionControls(for: entries)
} }
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
ForEach(entries) { entry in ForEach(entries) { entry in
row(for: entry) row(for: entry)
Divider().overlay(Color.flixySeparator) Divider().overlay(Color.haloSeparator)
} }
} }
} }
@@ -207,16 +207,16 @@ struct SeriesDetailView: View {
Button { Button {
if gewaehlt { selection.remove(entry.id) } else { selection.insert(entry.id) } if gewaehlt { selection.remove(entry.id) } else { selection.insert(entry.id) }
} label: { } label: {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
ChildRow(item: entry) ChildRow(item: entry)
Image(systemName: gewaehlt ? "checkmark.circle.fill" : "circle") Image(systemName: gewaehlt ? "checkmark.circle.fill" : "circle")
.font(.title3) .font(.title3)
.foregroundStyle(gewaehlt ? accent : Color.flixyTextMuted) .foregroundStyle(gewaehlt ? accent : Color.haloTextMuted)
} }
.contentShape(.rect) .contentShape(.rect)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
.accessibilityAddTraits(gewaehlt ? .isSelected : []) .accessibilityAddTraits(gewaehlt ? .isSelected : [])
} else { } else {
// **Der Knopf steht neben dem Link, nicht darin.** Ein `Button` im // **Der Knopf steht neben dem Link, nicht darin.** Ein `Button` im
@@ -228,7 +228,7 @@ struct SeriesDetailView: View {
// Als Geschwister in einer Reihe statt als Überzug: dann teilt das // Als Geschwister in einer Reihe statt als Überzug: dann teilt das
// Layout den Platz auf, und der Titel kann gar nicht erst unter // Layout den Platz auf, und der Titel kann gar nicht erst unter
// dem Knopf verschwinden. // dem Knopf verschwinden.
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
NavigationLink(value: MediaRoute(ratingKey: entry.id, title: entry.title)) { NavigationLink(value: MediaRoute(ratingKey: entry.id, title: entry.title)) {
ChildRow(item: entry) ChildRow(item: entry)
} }
@@ -241,7 +241,7 @@ struct SeriesDetailView: View {
DownloadButton(item: entry, loadsDetails: true) DownloadButton(item: entry, loadsDetails: true)
} }
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
} }
@@ -249,7 +249,7 @@ struct SeriesDetailView: View {
private func selectionControls(for entries: [MediaItem]) -> some View { private func selectionControls(for entries: [MediaItem]) -> some View {
let folgen = entries.filter { $0.kind == .episode }.map(\.id) let folgen = entries.filter { $0.kind == .episode }.map(\.id)
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
if isSelecting { if isSelecting {
Button(selection.count == folgen.count ? "detail.selectNone" : "detail.selectAll") { Button(selection.count == folgen.count ? "detail.selectNone" : "detail.selectAll") {
selection = selection.count == folgen.count ? [] : Set(folgen) selection = selection.count == folgen.count ? [] : Set(folgen)
@@ -258,12 +258,12 @@ struct SeriesDetailView: View {
isSelecting = false isSelecting = false
selection = [] selection = []
} }
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} else { } else {
Button("detail.select") { isSelecting = true } Button("detail.select") { isSelecting = true }
} }
} }
.font(.flixyCaption) .font(.haloCaption)
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundStyle(accent) .foregroundStyle(accent)
} }
@@ -288,14 +288,14 @@ struct SeriesDetailView: View {
) )
} }
} }
.font(.flixyCardTitle) .font(.haloCardTitle)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
.disabled(isWorking) .disabled(isWorking)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(.bar) .background(.bar)
} }
@@ -387,11 +387,11 @@ struct SeriesDetailView: View {
/// Eine Staffel- oder Folgenzeile. /// Eine Staffel- oder Folgenzeile.
private struct ChildRow: View { private struct ChildRow: View {
let item: MediaItem let item: MediaItem
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
var body: some View { var body: some View {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
FlixyArtwork( HaloArtwork(
url: session.backend?.artworkURL( url: session.backend?.artworkURL(
path: item.thumb, path: item.thumb,
width: item.kind == .season ? 120 : 240, width: item.kind == .season ? 120 : 240,
@@ -411,14 +411,14 @@ private struct ChildRow: View {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(rowTitle) Text(rowTitle)
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(2) .lineLimit(2)
if let subtitle { if let subtitle {
Text(subtitle) Text(subtitle)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.lineLimit(1) .lineLimit(1)
} }
} }
@@ -427,14 +427,14 @@ private struct ChildRow: View {
if item.isWatched { if item.isWatched {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.accessibilityLabel(Text("item.watched")) .accessibilityLabel(Text("item.watched"))
} }
} }
// Das waagerechte Polster liegt jetzt eine Ebene höher, bei der // Das waagerechte Polster liegt jetzt eine Ebene höher, bei der
// Reihe aus Zeile und Download-Knopf — sonst klebte der Knopf am // Reihe aus Zeile und Download-Knopf — sonst klebte der Knopf am
// Bildschirmrand. // Bildschirmrand.
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.contentShape(.rect) .contentShape(.rect)
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
} }
@@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyPlayback import HaloPlayback
import MediaKit import MediaKit
/// Die aufklappbaren technischen Angaben zu einer Datei. /// Die aufklappbaren technischen Angaben zu einer Datei.
/// ///
/// Zeigt nicht nur, was in der Datei steckt, sondern auch, wie Flixy sie /// Zeigt nicht nur, was in der Datei steckt, sondern auch, wie Halo sie
/// abspielen wird und warum. Bei der Frage „warum ruckelt das" ist das die /// abspielen wird und warum. Bei der Frage „warum ruckelt das" ist das die
/// erste Stelle zum Nachsehen — und in Phase 1, wo es noch kein Transcoding /// erste Stelle zum Nachsehen — und in Phase 1, wo es noch kein Transcoding
/// gibt, erklärt es, warum bei manchen Dateien kein Bild-in-Bild angeboten wird. /// gibt, erklärt es, warum bei manchen Dateien kein Bild-in-Bild angeboten wird.
@@ -34,13 +34,13 @@ struct TechnicalDetails: View {
var body: some View { var body: some View {
DisclosureGroup(isExpanded: $isExpanded) { DisclosureGroup(isExpanded: $isExpanded) {
VStack(alignment: .leading, spacing: FlixySpacing.snug) { VStack(alignment: .leading, spacing: HaloSpacing.snug) {
row("technical.playbackMethod", playbackMethodLabel) row("technical.playbackMethod", playbackMethodLabel)
if let reason = rationaleLabel { if let reason = rationaleLabel {
row("technical.reason", reason) row("technical.reason", reason)
} }
Divider().overlay(Color.flixySeparator).padding(.vertical, FlixySpacing.tight) Divider().overlay(Color.haloSeparator).padding(.vertical, HaloSpacing.tight)
if let container = part.container { row("technical.container", container.uppercased()) } if let container = part.container { row("technical.container", container.uppercased()) }
if let codec = media.videoCodec { row("technical.videoCodec", codec.uppercased()) } if let codec = media.videoCodec { row("technical.videoCodec", codec.uppercased()) }
@@ -55,7 +55,7 @@ struct TechnicalDetails: View {
} }
if !part.audioStreams.isEmpty { if !part.audioStreams.isEmpty {
Divider().overlay(Color.flixySeparator).padding(.vertical, FlixySpacing.tight) Divider().overlay(Color.haloSeparator).padding(.vertical, HaloSpacing.tight)
ForEach(part.audioStreams) { stream in ForEach(part.audioStreams) { stream in
row("technical.audio", trackLabel(stream)) row("technical.audio", trackLabel(stream))
} }
@@ -68,36 +68,36 @@ struct TechnicalDetails: View {
} }
if let file = part.file { if let file = part.file {
Divider().overlay(Color.flixySeparator).padding(.vertical, FlixySpacing.tight) Divider().overlay(Color.haloSeparator).padding(.vertical, HaloSpacing.tight)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("technical.file") Text("technical.file")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
Text(file) Text(file)
.font(.system(.caption, design: .monospaced)) .font(.system(.caption, design: .monospaced))
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.textSelection(.enabled) .textSelection(.enabled)
} }
} }
} }
.padding(.top, FlixySpacing.snug) .padding(.top, HaloSpacing.snug)
} label: { } label: {
Text("technical.title") Text("technical.title")
.font(.flixyRowHeader) .font(.haloRowHeader)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
} }
.tint(Color.flixyTextSecondary) .tint(Color.haloTextSecondary)
} }
private func row(_ label: LocalizedStringKey, _ value: String) -> some View { private func row(_ label: LocalizedStringKey, _ value: String) -> some View {
HStack(alignment: .top) { HStack(alignment: .top) {
Text(label) Text(label)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
Spacer() Spacer()
Text(value) Text(value)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
} }
} }
@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyDownloads import HaloDownloads
import MediaKit import MediaKit
/// Der Download-Knopf auf einer Detailseite. /// Der Download-Knopf auf einer Detailseite.
@@ -25,10 +25,10 @@ struct DownloadButton: View {
/// nicht drückt. /// nicht drückt.
var loadsDetails: Bool = false var loadsDetails: Bool = false
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(LanguagePreferences.self) private var languages @Environment(LanguagePreferences.self) private var languages
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var refusal: DownloadRefusal? @State private var refusal: DownloadRefusal?
@State private var showsQualityChoice = false @State private var showsQualityChoice = false
@@ -64,18 +64,18 @@ struct DownloadButton: View {
} else { } else {
Label(label, systemImage: symbol) Label(label, systemImage: symbol)
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.flixyCardTitle) .font(.haloCardTitle)
} }
} }
// Feste Größe, damit der Knopf beim Nachladen nicht springt: der // Feste Größe, damit der Knopf beim Nachladen nicht springt: der
// Kreisel ist schmaler als das Symbol, und eine Zeile, die während // Kreisel ist schmaler als das Symbol, und eine Zeile, die während
// des Wartens die Breite wechselt, sieht aus wie ein Fehler. // des Wartens die Breite wechselt, sieht aus wie ein Fehler.
.frame(width: 22, height: 22) .frame(width: 22, height: 22)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.disabled(isLoadingDetails) .disabled(isLoadingDetails)
.buttonStyle(.bordered) .buttonStyle(.bordered)
.tint(existing?.state == .completed ? accent : Color.flixyTextSecondary) .tint(existing?.state == .completed ? accent : Color.haloTextSecondary)
.accessibilityLabel(Text(label)) .accessibilityLabel(Text(label))
.confirmationDialog( .confirmationDialog(
Text("downloads.quality.title"), Text("downloads.quality.title"),
@@ -247,10 +247,10 @@ extension DownloadQuality {
struct DownloadAllButton: View { struct DownloadAllButton: View {
let container: MediaItem let container: MediaItem
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(LanguagePreferences.self) private var languages @Environment(LanguagePreferences.self) private var languages
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var isWorking = false @State private var isWorking = false
@State private var summary: String? @State private var summary: String?
@@ -269,7 +269,7 @@ struct DownloadAllButton: View {
} }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
.tint(Color.flixyTextSecondary) .tint(Color.haloTextSecondary)
.disabled(isWorking) .disabled(isWorking)
.alert( .alert(
Text("downloads.batch.title"), Text("downloads.batch.title"),
@@ -1,5 +1,5 @@
import Foundation import Foundation
import FlixyDownloads import HaloDownloads
import MediaKit import MediaKit
/// Welche Qualitätsstufen für einen Titel zur Wahl stehen — und wie sie heißen. /// Welche Qualitätsstufen für einen Titel zur Wahl stehen — und wie sie heißen.
@@ -1,14 +1,14 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyDownloads import HaloDownloads
import FlixyPlayback import HaloPlayback
/// Was offline ist und was gerade geladen wird. /// Was offline ist und was gerade geladen wird.
struct DownloadsView: View { struct DownloadsView: View {
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(PlaybackCoordinator.self) private var playback @Environment(PlaybackCoordinator.self) private var playback
@Environment(NetworkConditions.self) private var network @Environment(NetworkConditions.self) private var network
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.horizontalSizeClass) private var sizeClass
@@ -24,16 +24,16 @@ struct DownloadsView: View {
list list
} }
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("sidebar.downloads")) .navigationTitle(Text("sidebar.downloads"))
.toolbar { .toolbar {
// Nur auf dem iPhone: dort gibt es den Einstellungen-Tab nicht // Nur auf dem iPhone: dort gibt es den Einstellungen-Tab nicht
// mehr, und dies ist der Weg dorthin. // mehr, und dies ist der Weg dorthin.
if sizeClass == .compact { if sizeClass == .compact {
ToolbarItem(placement: .flixyTrailing) { AccountToolbarButton() } ToolbarItem(placement: .haloTrailing) { AccountToolbarButton() }
} }
} }
.flixyInlineTitle() .haloInlineTitle()
// Keine Rückfrage beim Entfernen. // Keine Rückfrage beim Entfernen.
// //
// Sie stand hier, weil „löschen" nach etwas Endgültigem klingt. Es ist // Sie stand hier, weil „löschen" nach etwas Endgültigem klingt. Es ist
@@ -88,8 +88,8 @@ struct DownloadsView: View {
} }
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
private var active: [DownloadItem] { private var active: [DownloadItem] {
@@ -125,7 +125,7 @@ struct DownloadsView: View {
/// Die Kopfzeile mit Speicherbelegung und Schaltern. /// Die Kopfzeile mit Speicherbelegung und Schaltern.
private struct StorageSection: View { private struct StorageSection: View {
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
@Bindable var downloads = downloads @Bindable var downloads = downloads
@@ -133,10 +133,10 @@ private struct StorageSection: View {
Section { Section {
HStack { HStack {
Text("downloads.used") Text("downloads.used")
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
Spacer() Spacer()
Text(downloads.usedBytes.formatted(.byteCount(style: .file))) Text(downloads.usedBytes.formatted(.byteCount(style: .file)))
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
} }
if let limit = downloads.storageLimitBytes, limit > 0 { if let limit = downloads.storageLimitBytes, limit > 0 {
@@ -157,20 +157,20 @@ private struct DownloadRow: View {
let onRemove: () -> Void let onRemove: () -> Void
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.snug) { VStack(alignment: .leading, spacing: HaloSpacing.snug) {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(item.title) Text(item.title)
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(1) .lineLimit(1)
if let subtitle = item.subtitle { if let subtitle = item.subtitle {
Text(subtitle) Text(subtitle)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.lineLimit(1) .lineLimit(1)
} }
} }
@@ -190,19 +190,19 @@ private struct DownloadRow: View {
hold == .offline ? "downloads.hold.offline" : "downloads.hold.wifi", hold == .offline ? "downloads.hold.offline" : "downloads.hold.wifi",
systemImage: hold == .offline ? "wifi.slash" : "wifi.exclamationmark" systemImage: hold == .offline ? "wifi.slash" : "wifi.exclamationmark"
) )
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
if let message = item.failureMessage { if let message = item.failureMessage {
Text(message) Text(message)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyDestructive) .foregroundStyle(Color.haloDestructive)
.lineLimit(2) .lineLimit(2)
} }
} }
.padding(.vertical, FlixySpacing.tight) .padding(.vertical, HaloSpacing.tight)
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button("action.delete", role: .destructive, action: onRemove) Button("action.delete", role: .destructive, action: onRemove)
} }
@@ -240,18 +240,18 @@ private struct DownloadRow: View {
case .completed: case .completed:
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.accessibilityLabel(Text("downloads.available")) .accessibilityLabel(Text("downloads.available"))
case .queued, .waiting: case .queued, .waiting:
Image(systemName: "clock") Image(systemName: "clock")
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.accessibilityLabel(Text("downloads.queued")) .accessibilityLabel(Text("downloads.queued"))
} }
} }
private var progress: some View { private var progress: some View {
VStack(alignment: .leading, spacing: FlixySpacing.tight) { VStack(alignment: .leading, spacing: HaloSpacing.tight) {
// Ohne bekannte Gesamtgröße ein unbestimmter Balken: ein Balken bei // Ohne bekannte Gesamtgröße ein unbestimmter Balken: ein Balken bei
// null Prozent, der sich nie bewegt, sieht aus wie ein hängender // null Prozent, der sich nie bewegt, sieht aus wie ein hängender
// Download. // Download.
@@ -273,17 +273,17 @@ private struct DownloadRow: View {
// gemeinsames Mass für beide Zahlen (siehe `unit`), und die // gemeinsames Mass für beide Zahlen (siehe `unit`), und die
// Prozente am rechten Rand — die wachsen nach innen und schieben // Prozente am rechten Rand — die wachsen nach innen und schieben
// nichts. // nichts.
HStack(alignment: .firstTextBaseline, spacing: FlixySpacing.snug) { HStack(alignment: .firstTextBaseline, spacing: HaloSpacing.snug) {
Text(statusLine) Text(statusLine)
.lineLimit(1) .lineLimit(1)
Spacer(minLength: FlixySpacing.snug) Spacer(minLength: HaloSpacing.snug)
if let percentage { if let percentage {
Text(percentage) Text(percentage)
} }
} }
.font(.flixyCaption) .font(.haloCaption)
.monospacedDigit() .monospacedDigit()
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
} }
@@ -387,28 +387,28 @@ private struct DownloadRow: View {
private struct HoldNotice: View { private struct HoldNotice: View {
let hold: TransferHold let hold: TransferHold
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
HStack(alignment: .top, spacing: FlixySpacing.regular) { HStack(alignment: .top, spacing: HaloSpacing.regular) {
Image(systemName: hold == .offline ? "wifi.slash" : "wifi.exclamationmark") Image(systemName: hold == .offline ? "wifi.slash" : "wifi.exclamationmark")
.font(.title3) .font(.title3)
.foregroundStyle(accent) .foregroundStyle(accent)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(hold == .offline ? "downloads.hold.offline" : "downloads.hold.wifi") Text(hold == .offline ? "downloads.hold.offline" : "downloads.hold.wifi")
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Text(hold == .offline Text(hold == .offline
? "downloads.hold.offline.description" ? "downloads.hold.offline.description"
: "downloads.hold.wifi.description") : "downloads.hold.wifi.description")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
.padding(.vertical, FlixySpacing.tight) .padding(.vertical, HaloSpacing.tight)
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Der Aufmacher oben auf der Startseite — mehrere Titel, die sich abwechseln. /// Der Aufmacher oben auf der Startseite — mehrere Titel, die sich abwechseln.
@@ -65,8 +65,8 @@ struct HeroCarousel: View {
@State private var isHoveringDots = false @State private var isHoveringDots = false
@Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.horizontalSizeClass) private var sizeClass
@Environment(TopChrome.self) private var chrome @Environment(TopChrome.self) private var chrome
@Environment(HomeLayoutSettings.self) private var layout @Environment(HomeLayoutSettings.self) private var layout
@@ -118,7 +118,7 @@ struct HeroCarousel: View {
} }
var body: some View { var body: some View {
VStack(spacing: FlixySpacing.regular) { VStack(spacing: HaloSpacing.regular) {
heroSection heroSection
// Die Höhe: fast das ganze Fenster. // Die Höhe: fast das ganze Fenster.
// //
@@ -189,7 +189,7 @@ struct HeroCarousel: View {
} }
} }
private var isMac: Bool { FlixyPlatform.isMac } private var isMac: Bool { HaloPlatform.isMac }
/// Die beiden Pfeile links und rechts. /// Die beiden Pfeile links und rechts.
/// ///
@@ -203,7 +203,7 @@ struct HeroCarousel: View {
Spacer() Spacer()
arrow(symbol: "chevron.right", direction: 1) arrow(symbol: "chevron.right", direction: 1)
} }
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.opacity(isPointerInside ? 1 : 0) .opacity(isPointerInside ? 1 : 0)
.animation(.easeOut(duration: 0.2), value: isPointerInside) .animation(.easeOut(duration: 0.2), value: isPointerInside)
.onContinuousHover { phase in .onContinuousHover { phase in
@@ -219,7 +219,7 @@ struct HeroCarousel: View {
Image(systemName: symbol) Image(systemName: symbol)
.font(.system(size: 18, weight: .semibold)) .font(.system(size: 18, weight: .semibold))
.foregroundStyle(.white) .foregroundStyle(.white)
.padding(FlixySpacing.regular) .padding(HaloSpacing.regular)
.background(.black.opacity(0.45), in: .circle) .background(.black.opacity(0.45), in: .circle)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -237,10 +237,10 @@ struct HeroCarousel: View {
/// Auf dem iPhone und dem iPad bleibt es eine Anzeige. Dort gibt es /// Auf dem iPhone und dem iPad bleibt es eine Anzeige. Dort gibt es
/// keinen Zeiger, der sich nähern könnte, und getippt wird gewischt. /// keinen Zeiger, der sich nähern könnte, und getippt wird gewischt.
private var indicator: some View { private var indicator: some View {
HStack(spacing: isEnlarged ? FlixySpacing.regular : FlixySpacing.snug) { HStack(spacing: isEnlarged ? HaloSpacing.regular : HaloSpacing.snug) {
ForEach(items.indices, id: \.self) { position in ForEach(items.indices, id: \.self) { position in
Capsule() Capsule()
.fill(position == currentIndex ? accent : Color.flixyTextMuted.opacity(0.4)) .fill(position == currentIndex ? accent : Color.haloTextMuted.opacity(0.4))
.frame( .frame(
width: position == currentIndex ? (isEnlarged ? 28 : 18) : (isEnlarged ? 12 : 6), width: position == currentIndex ? (isEnlarged ? 28 : 18) : (isEnlarged ? 12 : 6),
height: isEnlarged ? 12 : 6 height: isEnlarged ? 12 : 6
@@ -255,7 +255,7 @@ struct HeroCarousel: View {
} }
// Eine grössere Fläche als die Punkte selbst: der Zeiger soll schon // Eine grössere Fläche als die Punkte selbst: der Zeiger soll schon
// beim Annähern etwas bewirken, nicht erst beim Treffen. // beim Annähern etwas bewirken, nicht erst beim Treffen.
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.contentShape(.rect) .contentShape(.rect)
.onContinuousHover { phase in .onContinuousHover { phase in
guard isMac else { return } guard isMac else { return }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Der Aufmacher oben auf der Startseite. /// Der Aufmacher oben auf der Startseite.
@@ -12,10 +12,10 @@ struct HeroSection: View {
/// Wie weit es dabei hereinrutscht. /// Wie weit es dabei hereinrutscht.
var incomingOffset: CGFloat = 0 var incomingOffset: CGFloat = 0
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(PlaybackCoordinator.self) private var playback @Environment(PlaybackCoordinator.self) private var playback
@Environment(QuickPlay.self) private var quickPlay @Environment(QuickPlay.self) private var quickPlay
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.horizontalSizeClass) private var sizeClass
var body: some View { var body: some View {
@@ -56,21 +56,21 @@ struct HeroSection: View {
quickPlay.start(item, backend: session.backend, coordinator: playback) quickPlay.start(item, backend: session.backend, coordinator: playback)
} label: { } label: {
Label(playLabel, systemImage: "play.fill") Label(playLabel, systemImage: "play.fill")
.font(.flixyCardTitle) .font(.haloCardTitle)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) { NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) {
Label("hero.details", systemImage: "info.circle") Label("hero.details", systemImage: "info.circle")
.font(.flixyCardTitle) .font(.haloCardTitle)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
.tint(Color.flixyTextSecondary) .tint(Color.haloTextSecondary)
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Die Startseite: Aufmacher oben, darunter die Reihen vom Server. /// Die Startseite: Aufmacher oben, darunter die Reihen vom Server.
@@ -9,7 +9,7 @@ import MediaKit
/// Reihen zeigt und die Logik sonst zweimal dastünde. Kopiert wäre dabei /// Reihen zeigt und die Logik sonst zweimal dastünde. Kopiert wäre dabei
/// ausgerechnet der Teil gewesen, an dem schon mehrere Fehler steckten. /// ausgerechnet der Teil gewesen, an dem schon mehrere Fehler steckten.
struct HomeView: View { struct HomeView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(HomeLayoutSettings.self) private var layout @Environment(HomeLayoutSettings.self) private var layout
@State private var feed = HomeFeed() @State private var feed = HomeFeed()
@@ -41,17 +41,17 @@ struct HomeView: View {
} }
} }
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
// Kein Titel: der Name der App gehört auf den Homescreen, nicht // Kein Titel: der Name der App gehört auf den Homescreen, nicht
// über jeden Bildschirm. Darunter liegt der Aufmacher, und der sagt // über jeden Bildschirm. Darunter liegt der Aufmacher, und der sagt
// ohnehin mehr. // ohnehin mehr.
.navigationTitle("") .navigationTitle("")
.flixyInlineTitle() .haloInlineTitle()
// Gar kein Hintergrund statt eines weichgezeichneten: die Knöpfe // Gar kein Hintergrund statt eines weichgezeichneten: die Knöpfe
// bringen ihre eigene Glasfläche mit und bleiben auch über dem Bild // bringen ihre eigene Glasfläche mit und bleiben auch über dem Bild
// lesbar. `.ultraThinMaterial` verschleierte dagegen den halben // lesbar. `.ultraThinMaterial` verschleierte dagegen den halben
// Aufmacher. // Aufmacher.
.flixyClearToolbarBackground() .haloClearToolbarBackground()
// Gar keine Navigationsleiste auf der Startseite. // Gar keine Navigationsleiste auf der Startseite.
// //
// Sie war leer — kein Titel, keine Knöpfe — und belegte trotzdem ihre // Sie war leer — kein Titel, keine Knöpfe — und belegte trotzdem ihre
@@ -63,12 +63,12 @@ struct HomeView: View {
.refreshable { await feed.load(from: session.backend) } .refreshable { await feed.load(from: session.backend) }
// Was eine Kachel am Server ändert, ändert auch, was hier steht: // Was eine Kachel am Server ändert, ändert auch, was hier steht:
// eine abgehakte Folge gehört nicht mehr in „Weiterschauen". // eine abgehakte Folge gehört nicht mehr in „Weiterschauen".
.environment(\.flixyItemChanged, { await feed.load(from: session.backend) }) .environment(\.haloItemChanged, { await feed.load(from: session.backend) })
} }
private func content(hubs: [MediaRowGroup]) -> some View { private func content(hubs: [MediaRowGroup]) -> some View {
ScrollView { ScrollView {
LazyVStack(alignment: .leading, spacing: FlixySpacing.section) { LazyVStack(alignment: .leading, spacing: HaloSpacing.section) {
let featured = feed.heroItems(for: layout) let featured = feed.heroItems(for: layout)
if !featured.isEmpty { if !featured.isEmpty {
HeroCarousel(items: featured) HeroCarousel(items: featured)
@@ -78,9 +78,9 @@ struct HomeView: View {
MediaRow(row: hub) MediaRow(row: hub)
} }
} }
.padding(.bottom, FlixySpacing.section) .padding(.bottom, HaloSpacing.section)
} }
.scrollIndicators(.never) .scrollIndicators(.never)
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Eine waagerechte Reihe von Titeln. /// Eine waagerechte Reihe von Titeln.
@@ -8,7 +8,7 @@ struct MediaRow: View {
/// Wird durchgereicht — siehe `QuickPlayTile`. /// Wird durchgereicht — siehe `QuickPlayTile`.
var opensDetails: Bool = false var opensDetails: Bool = false
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(HomeLayoutSettings.self) private var layout @Environment(HomeLayoutSettings.self) private var layout
@@ -23,19 +23,19 @@ struct MediaRow: View {
private var posterWidth: CGFloat { 116 * layout.cardScale } private var posterWidth: CGFloat { 116 * layout.cardScale }
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
Text(row.title) Text(row.title)
.font(.flixyRowHeader) .font(.haloRowHeader)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
ScrollView(.horizontal) { ScrollView(.horizontal) {
LazyHStack(spacing: FlixySpacing.regular) { LazyHStack(spacing: HaloSpacing.regular) {
ForEach(row.items) { item in ForEach(row.items) { item in
QuickPlayTile(item: item, opensDetails: opensDetails) { card(for: item) } QuickPlayTile(item: item, opensDetails: opensDetails) { card(for: item) }
} }
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
// Ohne dieses Ziel rasten die Kacheln nicht ein und die Reihe // Ohne dieses Ziel rasten die Kacheln nicht ein und die Reihe
// bleibt an beliebiger Stelle stehen. // bleibt an beliebiger Stelle stehen.
.scrollTargetLayout() .scrollTargetLayout()
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Die Mediatheken auf dem iPhone — mit einer Leiste zum Wechseln. /// Die Mediatheken auf dem iPhone — mit einer Leiste zum Wechseln.
@@ -20,7 +20,7 @@ import MediaKit
/// dessen Einträge zwei Navigationsleisten übereinander tragen. Eine selbst /// dessen Einträge zwei Navigationsleisten übereinander tragen. Eine selbst
/// gezeichnete Leiste kennt diese Grenze nicht. /// gezeichnete Leiste kennt diese Grenze nicht.
struct LibraryBrowser: View { struct LibraryBrowser: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
/// Was gerade gezeigt wird. /// Was gerade gezeigt wird.
/// ///
@@ -36,7 +36,7 @@ struct LibraryBrowser: View {
var body: some View { var body: some View {
inhalt inhalt
.background(Color.flixyCanvas) .background(Color.haloCanvas)
// **Als Sicherheitsrand, nicht als Überzug.** So endet die Liste // **Als Sicherheitsrand, nicht als Überzug.** So endet die Liste
// über der Leiste, statt dahinter zu verschwinden — und das gilt // über der Leiste, statt dahinter zu verschwinden — und das gilt
// auch für die Ladeanzeige und den Fuss „alles geladen". // auch für die Ladeanzeige und den Fuss „alles geladen".
@@ -86,11 +86,11 @@ struct LibraryBrowser: View {
} }
// Dieselbe Fläche wie die Insel auf dem iPad — ein `static`-Bauteil, // Dieselbe Fläche wie die Insel auf dem iPad — ein `static`-Bauteil,
// kein nachgebautes Aussehen. // kein nachgebautes Aussehen.
.background { FlixySidebar.surface(in: .capsule) } .background { HaloSidebar.surface(in: .capsule) }
.clipShape(.capsule) .clipShape(.capsule)
.shadow(color: .black.opacity(0.45), radius: 18, y: 6) .shadow(color: .black.opacity(0.45), radius: 18, y: 6)
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.bottom, FlixySpacing.snug) .padding(.bottom, HaloSpacing.snug)
} }
private var reihe: some View { private var reihe: some View {
@@ -99,8 +99,8 @@ struct LibraryBrowser: View {
pille(eintrag) pille(eintrag)
} }
} }
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
private func pille(_ eintrag: Eintrag) -> some View { private func pille(_ eintrag: Eintrag) -> some View {
@@ -116,12 +116,12 @@ struct LibraryBrowser: View {
} }
// Kleiner als in der Leiste auf dem iPad: dort stehen drei // Kleiner als in der Leiste auf dem iPad: dort stehen drei
// Einträge, hier bis zu fünf — und auf einem Telefon. // Einträge, hier bis zu fünf — und auf einem Telefon.
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(gewaehlt ? Color.flixyTextPrimary : Color.flixyTextSecondary) .foregroundStyle(gewaehlt ? Color.haloTextPrimary : Color.haloTextSecondary)
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.background { .background {
if gewaehlt { Capsule().fill(Color.flixyElevated) } if gewaehlt { Capsule().fill(Color.haloElevated) }
} }
.contentShape(Capsule()) .contentShape(Capsule())
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Filter und Sortierung einer Mediathek. /// Filter und Sortierung einer Mediathek.
@@ -14,8 +14,8 @@ struct LibraryFilterSheet: View {
let library: MediaLibrary let library: MediaLibrary
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@State private var genres: [MediaFilterOption] = [] @State private var genres: [MediaFilterOption] = []
/// Ein Arbeitsstand, der erst beim Bestätigen übernommen wird. /// Ein Arbeitsstand, der erst beim Bestätigen übernommen wird.
@@ -91,11 +91,11 @@ struct LibraryFilterSheet: View {
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe // gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
// leuchteten, während die Schalter daneben schon die gewählte trugen. // leuchteten, während die Schalter daneben schon die gewählte trugen.
.tint(accent) .tint(accent)
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("filter.title")) .navigationTitle(Text("filter.title"))
.flixyInlineTitle() .haloInlineTitle()
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("action.cancel") { dismiss() } Button("action.cancel") { dismiss() }
@@ -109,7 +109,7 @@ struct LibraryFilterSheet: View {
} }
.task { await loadGenres() } .task { await loadGenres() }
} }
.flixyResizableSheet() .haloResizableSheet()
} }
private func loadGenres() async { private func loadGenres() async {
@@ -1,16 +1,16 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Eine Mediathek zum Durchblättern. /// Eine Mediathek zum Durchblättern.
struct LibraryView: View { struct LibraryView: View {
let library: MediaLibrary let library: MediaLibrary
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
/// Nur für die Kachelgröße. „layout“ heißt hier schon die Wahl /// Nur für die Kachelgröße. „layout“ heißt hier schon die Wahl
/// zwischen Raster und Liste. /// zwischen Raster und Liste.
@Environment(HomeLayoutSettings.self) private var sizing @Environment(HomeLayoutSettings.self) private var sizing
@AppStorage("flixy.library.layout") private var layout: LibraryLayout = .grid @AppStorage("halo.library.layout") private var layout: LibraryLayout = .grid
@Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.horizontalSizeClass) private var sizeClass
@State private var items: [MediaItem] = [] @State private var items: [MediaItem] = []
@@ -59,9 +59,9 @@ struct LibraryView: View {
} }
} }
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(library.title) .navigationTitle(library.title)
.flixyInlineTitle() .haloInlineTitle()
.toolbar { toolbar } .toolbar { toolbar }
.sheet(isPresented: $showsFilters) { .sheet(isPresented: $showsFilters) {
LibraryFilterSheet(query: $query, library: library) LibraryFilterSheet(query: $query, library: library)
@@ -80,7 +80,7 @@ struct LibraryView: View {
.onChange(of: query) { _, _ in Task { await reload() } } .onChange(of: query) { _, _ in Task { await reload() } }
.refreshable { await reload() } .refreshable { await reload() }
// Siehe `HomeView`: was eine Kachel ändert, muss im Raster ankommen. // Siehe `HomeView`: was eine Kachel ändert, muss im Raster ankommen.
.environment(\.flixyItemChanged, { await reload() }) .environment(\.haloItemChanged, { await reload() })
} }
@ViewBuilder @ViewBuilder
@@ -97,8 +97,8 @@ struct LibraryView: View {
// Mindestbreite statt fester Spaltenzahl: dieselbe Ansicht // Mindestbreite statt fester Spaltenzahl: dieselbe Ansicht
// ergibt auf dem iPhone drei und auf dem iPad sechs Spalten, // ergibt auf dem iPhone drei und auf dem iPad sechs Spalten,
// ohne dass die Größenklasse abgefragt werden muss. // ohne dass die Größenklasse abgefragt werden muss.
columns: [GridItem(.adaptive(minimum: 110 * sizing.cardScale), spacing: FlixySpacing.regular)], columns: [GridItem(.adaptive(minimum: 110 * sizing.cardScale), spacing: HaloSpacing.regular)],
spacing: FlixySpacing.loose spacing: HaloSpacing.loose
) { ) {
ForEach(items) { item in ForEach(items) { item in
QuickPlayTile(item: item) { QuickPlayTile(item: item) {
@@ -119,12 +119,12 @@ struct LibraryView: View {
PlaceholderCard(index: position) PlaceholderCard(index: position)
} }
} }
.padding(FlixySpacing.screenMargin) .padding(HaloSpacing.screenMargin)
moreButton.padding(.bottom, FlixySpacing.section) moreButton.padding(.bottom, HaloSpacing.section)
footerCount.padding(.bottom, FlixySpacing.section) footerCount.padding(.bottom, HaloSpacing.section)
} }
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
private var list: some View { private var list: some View {
@@ -137,19 +137,19 @@ struct LibraryView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.task { await loadMoreIfNeeded(reaching: item) } .task { await loadMoreIfNeeded(reaching: item) }
Divider().overlay(Color.flixySeparator) Divider().overlay(Color.haloSeparator)
} }
} }
if pendingCount > 0 { if pendingCount > 0 {
ProgressView() ProgressView()
.padding(.vertical, FlixySpacing.section) .padding(.vertical, HaloSpacing.section)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
moreButton.padding(.vertical, FlixySpacing.section) moreButton.padding(.vertical, HaloSpacing.section)
footerCount.padding(.bottom, FlixySpacing.section) footerCount.padding(.bottom, HaloSpacing.section)
} }
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
/// Wie viele Kacheln noch kommen — und zwar **gemeldet**, nicht geraten. /// Wie viele Kacheln noch kommen — und zwar **gemeldet**, nicht geraten.
@@ -173,8 +173,8 @@ struct LibraryView: View {
private var footerCount: some View { private var footerCount: some View {
if let page, !page.hasMore, !items.isEmpty { if let page, !page.hasMore, !items.isEmpty {
Text("library.allLoaded \(items.count)") Text("library.allLoaded \(items.count)")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
@@ -183,10 +183,10 @@ struct LibraryView: View {
@ViewBuilder @ViewBuilder
private var moreButton: some View { private var moreButton: some View {
if loadMoreFailed, let page, page.hasMore { if loadMoreFailed, let page, page.hasMore {
VStack(spacing: FlixySpacing.snug) { VStack(spacing: HaloSpacing.snug) {
Text("library.loadMoreFailed") Text("library.loadMoreFailed")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
Button("library.loadMore") { Button("library.loadMore") {
Task { await loadMore() } Task { await loadMore() }
} }
@@ -206,7 +206,7 @@ struct LibraryView: View {
// steht der Knopf. Auf dem iPad steht die Suche in der Seitenleiste, // steht der Knopf. Auf dem iPad steht die Suche in der Seitenleiste,
// und ein zweiter Weg dorthin wäre nur Verdopplung. // und ein zweiter Weg dorthin wäre nur Verdopplung.
if sizeClass == .compact { if sizeClass == .compact {
ToolbarItem(placement: .flixyTrailing) { ToolbarItem(placement: .haloTrailing) {
NavigationLink(value: SearchRoute()) { NavigationLink(value: SearchRoute()) {
Image(systemName: "magnifyingglass") Image(systemName: "magnifyingglass")
} }
@@ -218,7 +218,7 @@ struct LibraryView: View {
// oben, und selbst dort wo es das gibt, ist es in einem Raster kaum // oben, und selbst dort wo es das gibt, ist es in einem Raster kaum
// zu treffen. Wer den Verdacht hat, dass etwas fehlt, braucht einen // zu treffen. Wer den Verdacht hat, dass etwas fehlt, braucht einen
// Knopf und keine Geste. // Knopf und keine Geste.
ToolbarItem(placement: .flixyTrailing) { ToolbarItem(placement: .haloTrailing) {
Button { Button {
Task { await reload() } Task { await reload() }
} label: { } label: {
@@ -228,7 +228,7 @@ struct LibraryView: View {
.disabled(isLoadingMore) .disabled(isLoadingMore)
} }
ToolbarItem(placement: .flixyTrailing) { ToolbarItem(placement: .haloTrailing) {
Button { Button {
showsFilters = true showsFilters = true
} label: { } label: {
@@ -239,7 +239,7 @@ struct LibraryView: View {
.accessibilityLabel(Text("library.filters")) .accessibilityLabel(Text("library.filters"))
} }
ToolbarItem(placement: .flixyTrailing) { ToolbarItem(placement: .haloTrailing) {
Button { Button {
layout = layout == .grid ? .list : .grid layout = layout == .grid ? .list : .grid
} label: { } label: {
@@ -255,7 +255,7 @@ struct LibraryView: View {
// Reihe etwas steht, kann nur der entscheiden, der die Reihe // Reihe etwas steht, kann nur der entscheiden, der die Reihe
// schreibt. // schreibt.
if sizeClass == .compact { if sizeClass == .compact {
ToolbarItem(placement: .flixyTrailing) { AccountToolbarButton() } ToolbarItem(placement: .haloTrailing) { AccountToolbarButton() }
} }
} }
@@ -380,11 +380,11 @@ enum LibraryLayout: String, CaseIterable {
/// Eine Zeile in der Listenansicht. /// Eine Zeile in der Listenansicht.
private struct LibraryListRow: View { private struct LibraryListRow: View {
let item: MediaItem let item: MediaItem
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
var body: some View { var body: some View {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
FlixyArtwork( HaloArtwork(
url: session.backend?.artworkURL(path: item.thumb, width: 120, height: 180), url: session.backend?.artworkURL(path: item.thumb, width: 120, height: 180),
aspectRatio: PosterCard.aspectRatio aspectRatio: PosterCard.aspectRatio
) )
@@ -393,13 +393,13 @@ private struct LibraryListRow: View {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(item.title) Text(item.title)
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(1) .lineLimit(1)
if let subtitle = item.cardSubtitle { if let subtitle = item.cardSubtitle {
Text(subtitle) Text(subtitle)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
} }
@@ -407,12 +407,12 @@ private struct LibraryListRow: View {
if item.isWatched { if item.isWatched {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.accessibilityLabel(Text("item.watched")) .accessibilityLabel(Text("item.watched"))
} }
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.contentShape(.rect) .contentShape(.rect)
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Welche Sender überhaupt angezeigt werden. /// Welche Sender überhaupt angezeigt werden.
@@ -12,8 +12,8 @@ struct ChannelSelectionSheet: View {
let channels: [LiveChannel] let channels: [LiveChannel]
@Binding var selection: Set<String> @Binding var selection: Set<String>
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var body: some View { var body: some View {
@@ -28,9 +28,9 @@ struct ChannelSelectionSheet: View {
} }
} }
.navigationTitle(Text("liveTV.selection")) .navigationTitle(Text("liveTV.selection"))
.flixyInlineTitle() .haloInlineTitle()
.toolbar { .toolbar {
ToolbarItem(placement: .flixyLeading) { ToolbarItem(placement: .haloLeading) {
// „Alle" statt „keine": eine leere Auswahl bedeutet // „Alle" statt „keine": eine leere Auswahl bedeutet
// ohnehin alle, und ein Knopf, der scheinbar nichts tut, // ohnehin alle, und ein Knopf, der scheinbar nichts tut,
// wäre verwirrend. // wäre verwirrend.
@@ -57,10 +57,10 @@ struct ChannelSelectionSheet: View {
selection.insert(channel.id) selection.insert(channel.id)
} }
} label: { } label: {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
FlixyArtwork( HaloArtwork(
url: session.backend?.artworkURL(path: channel.artworkPath, width: 200, height: 120), url: session.backend?.artworkURL(path: channel.artworkPath, width: 200, height: 120),
aspectRatio: FlixyAspectRatio.channelLogo, aspectRatio: HaloAspectRatio.channelLogo,
fallbackSymbol: "tv", fallbackSymbol: "tv",
contentMode: .fit contentMode: .fit
) )
@@ -69,17 +69,17 @@ struct ChannelSelectionSheet: View {
if let number = channel.number { if let number = channel.number {
Text(number) Text(number)
.font(.system(.caption, design: .monospaced)) .font(.system(.caption, design: .monospaced))
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
Text(channel.title) Text(channel.title)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Spacer() Spacer()
Image(systemName: isShown(channel) ? "checkmark.circle.fill" : "circle") Image(systemName: isShown(channel) ? "checkmark.circle.fill" : "circle")
.foregroundStyle(isShown(channel) ? accent : Color.flixyTextMuted) .foregroundStyle(isShown(channel) ? accent : Color.haloTextMuted)
} }
.contentShape(.rect) .contentShape(.rect)
} }
@@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyPlayback import HaloPlayback
import MediaKit import MediaKit
/// Live-TV mit Programmführer. /// Live-TV mit Programmführer.
struct LiveTVView: View { struct LiveTVView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(PlaybackCoordinator.self) private var playback @Environment(PlaybackCoordinator.self) private var playback
@Environment(PlaybackSettings.self) private var settings @Environment(PlaybackSettings.self) private var settings
@Environment(NetworkConditions.self) private var network @Environment(NetworkConditions.self) private var network
@@ -74,7 +74,7 @@ struct LiveTVView: View {
} description: { } description: {
Text("liveTV.noChannels.description") Text("liveTV.noChannels.description")
} actions: { } actions: {
VStack(spacing: FlixySpacing.regular) { VStack(spacing: HaloSpacing.regular) {
Button("action.retry") { Task { await load() } } Button("action.retry") { Task { await load() } }
searchTrace searchTrace
} }
@@ -107,12 +107,12 @@ struct LiveTVView: View {
} }
} }
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("sidebar.liveTV")) .navigationTitle(Text("sidebar.liveTV"))
.flixyInlineTitle() .haloInlineTitle()
.toolbar { .toolbar {
if case .available = availability, !channels.isEmpty { if case .available = availability, !channels.isEmpty {
ToolbarItem(placement: .flixyTrailing) { ToolbarItem(placement: .haloTrailing) {
Button { Button {
showsSelection = true showsSelection = true
} label: { } label: {
@@ -120,7 +120,7 @@ struct LiveTVView: View {
} }
.accessibilityLabel(Text("liveTV.selection")) .accessibilityLabel(Text("liveTV.selection"))
} }
ToolbarItem(placement: .flixyTrailing) { ToolbarItem(placement: .haloTrailing) {
Picker("liveTV.mode", selection: $mode) { Picker("liveTV.mode", selection: $mode) {
Image(systemName: "calendar.day.timeline.left").tag(LiveTVMode.guide) Image(systemName: "calendar.day.timeline.left").tag(LiveTVMode.guide)
Image(systemName: "list.bullet").tag(LiveTVMode.channels) Image(systemName: "list.bullet").tag(LiveTVMode.channels)
@@ -169,11 +169,11 @@ struct LiveTVView: View {
@ViewBuilder @ViewBuilder
private var searchTrace: some View { private var searchTrace: some View {
if !trace.isEmpty { if !trace.isEmpty {
VStack(alignment: .leading, spacing: FlixySpacing.snug) { VStack(alignment: .leading, spacing: HaloSpacing.snug) {
if let channelSource { if let channelSource {
Text(channelSource) Text(channelSource)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
ScrollView { ScrollView {
@@ -181,7 +181,7 @@ struct LiveTVView: View {
ForEach(trace) { probe in ForEach(trace) { probe in
Text("\(probe.path) → \(probe.result)") Text("\(probe.path) → \(probe.result)")
.font(.system(size: 10, design: .monospaced)) .font(.system(size: 10, design: .monospaced))
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.lineLimit(2) .lineLimit(2)
.textSelection(.enabled) .textSelection(.enabled)
} }
@@ -191,18 +191,18 @@ struct LiveTVView: View {
.frame(maxHeight: 200) .frame(maxHeight: 200)
Button { Button {
FlixyClipboard.copy( HaloClipboard.copy(
trace trace
.map { "\($0.path) → \($0.result)" } .map { "\($0.path) → \($0.result)" }
.joined(separator: "\n") .joined(separator: "\n")
) )
} label: { } label: {
Label("diagnostics.copy", systemImage: "doc.on.doc") Label("diagnostics.copy", systemImage: "doc.on.doc")
.font(.flixyCaption) .font(.haloCaption)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
} }
} }
@@ -216,11 +216,11 @@ struct LiveTVView: View {
isTuning: isTuning == channel.id, isTuning: isTuning == channel.id,
onTune: { Task { await tune(channel) } } onTune: { Task { await tune(channel) } }
) )
Divider().overlay(Color.flixySeparator) Divider().overlay(Color.haloSeparator)
} }
} }
} }
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
private func load() async { private func load() async {
@@ -329,43 +329,43 @@ private struct ChannelRow: View {
let isTuning: Bool let isTuning: Bool
let onTune: () -> Void let onTune: () -> Void
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
private var current: LiveProgram? { programs.first { $0.isRunning() } } private var current: LiveProgram? { programs.first { $0.isRunning() } }
private var next: LiveProgram? { programs.first { $0.startsAt > Date() } } private var next: LiveProgram? { programs.first { $0.startsAt > Date() } }
var body: some View { var body: some View {
Button(action: onTune) { Button(action: onTune) {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
// Breiter als hoch und ohne Beschneiden: Sendersymbole sind // Breiter als hoch und ohne Beschneiden: Sendersymbole sind
// fast nie quadratisch, und formatfüllend fehlten links und // fast nie quadratisch, und formatfüllend fehlten links und
// rechts genau die Teile, an denen man den Sender erkennt. // rechts genau die Teile, an denen man den Sender erkennt.
FlixyArtwork( HaloArtwork(
url: session.backend?.artworkURL(path: channel.artworkPath, width: 200, height: 120), url: session.backend?.artworkURL(path: channel.artworkPath, width: 200, height: 120),
aspectRatio: FlixyAspectRatio.channelLogo, aspectRatio: HaloAspectRatio.channelLogo,
fallbackSymbol: "tv", fallbackSymbol: "tv",
contentMode: .fit contentMode: .fit
) )
.frame(width: 56, height: 34) .frame(width: 56, height: 34)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
if let number = channel.number { if let number = channel.number {
Text(number) Text(number)
.font(.system(.caption, design: .monospaced)) .font(.system(.caption, design: .monospaced))
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
Text(channel.title) Text(channel.title)
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(1) .lineLimit(1)
} }
if let current { if let current {
Text(current.title) Text(current.title)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.lineLimit(1) .lineLimit(1)
// Der Balken zeigt, wie weit die Sendung ist — beim // Der Balken zeigt, wie weit die Sendung ist — beim
// Umschalten die nützlichste Angabe überhaupt. // Umschalten die nützlichste Angabe überhaupt.
@@ -373,14 +373,14 @@ private struct ChannelRow: View {
.padding(.top, 2) .padding(.top, 2)
} else { } else {
Text("liveTV.noProgram") Text("liveTV.noProgram")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
if let next { if let next {
Text("liveTV.next \(next.startsAt.formatted(date: .omitted, time: .shortened)) \(next.title)") Text("liveTV.next \(next.startsAt.formatted(date: .omitted, time: .shortened)) \(next.title)")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.lineLimit(1) .lineLimit(1)
} }
} }
@@ -395,8 +395,8 @@ private struct ChannelRow: View {
.foregroundStyle(accent) .foregroundStyle(accent)
} }
} }
.padding(.horizontal, FlixySpacing.screenMargin) .padding(.horizontal, HaloSpacing.screenMargin)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
.contentShape(.rect) .contentShape(.rect)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -427,43 +427,43 @@ private struct TuneFailureSheet: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
Text(failure.channel) Text(failure.channel)
.font(.flixyRowHeader) .font(.haloRowHeader)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Text(failure.reason) Text(failure.reason)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
ForEach(Array(failure.trace.enumerated()), id: \.offset) { _, line in ForEach(Array(failure.trace.enumerated()), id: \.offset) { _, line in
Text(line) Text(line)
.font(.system(size: 11, design: .monospaced)) .font(.system(size: 11, design: .monospaced))
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.textSelection(.enabled) .textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
Button { Button {
FlixyClipboard.copy(failure.trace.joined(separator: "\n")) HaloClipboard.copy(failure.trace.joined(separator: "\n"))
} label: { } label: {
Label("diagnostics.copy", systemImage: "doc.on.doc") Label("diagnostics.copy", systemImage: "doc.on.doc")
.font(.flixyCaption) .font(.haloCaption)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
.padding(FlixySpacing.screenMargin) .padding(HaloSpacing.screenMargin)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("liveTV.tuneFailed")) .navigationTitle(Text("liveTV.tuneFailed"))
.flixyInlineTitle() .haloInlineTitle()
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button("action.close") { dismiss() } Button("action.close") { dismiss() }
} }
} }
} }
.flixyResizableSheet() .haloResizableSheet()
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Das Programmraster: Sender als Zeilen, Zeit als Achse. /// Das Programmraster: Sender als Zeilen, Zeit als Achse.
@@ -34,7 +34,7 @@ struct ProgramGuide: View {
var onRecord: ((LiveProgram) -> Void)? var onRecord: ((LiveProgram) -> Void)?
var onRecordSeries: ((LiveProgram) -> Void)? var onRecordSeries: ((LiveProgram) -> Void)?
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var scrollX: CGFloat = 0 @State private var scrollX: CGFloat = 0
@State private var selection: GuideSelection? @State private var selection: GuideSelection?
@@ -50,7 +50,7 @@ struct ProgramGuide: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
axis axis
Divider().overlay(Color.flixySeparator) Divider().overlay(Color.haloSeparator)
ScrollView(.vertical) { ScrollView(.vertical) {
HStack(alignment: .top, spacing: 0) { HStack(alignment: .top, spacing: 0) {
@@ -65,7 +65,7 @@ struct ProgramGuide: View {
} }
.scrollIndicators(.never) .scrollIndicators(.never)
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.sheet(item: $selection) { selection in .sheet(item: $selection) { selection in
ProgramDetailSheet( ProgramDetailSheet(
channel: selection.channel, channel: selection.channel,
@@ -104,12 +104,12 @@ struct ProgramGuide: View {
private var axis: some View { private var axis: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("liveTV.channelCount \(channels.count)") Text("liveTV.channelCount \(channels.count)")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
// Der Abstand gehört in die Spaltenbreite hinein, nicht // Der Abstand gehört in die Spaltenbreite hinein, nicht
// dazu: außen addiert er sich, und die ganze Zeitachse steht // dazu: außen addiert er sich, und die ganze Zeitachse steht
// acht Punkte neben dem Programm darunter. // acht Punkte neben dem Programm darunter.
.padding(.leading, FlixySpacing.snug) .padding(.leading, HaloSpacing.snug)
.frame(width: channelWidth, alignment: .leading) .frame(width: channelWidth, alignment: .leading)
.layoutPriority(1) .layoutPriority(1)
@@ -125,7 +125,7 @@ struct ProgramGuide: View {
ForEach(layout.ticks, id: \.self) { tick in ForEach(layout.ticks, id: \.self) { tick in
Text(tick.formatted(date: .omitted, time: .shortened)) Text(tick.formatted(date: .omitted, time: .shortened))
.font(.system(.caption2, design: .rounded)) .font(.system(.caption2, design: .rounded))
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.frame(width: layout.tickWidth, alignment: .leading) .frame(width: layout.tickWidth, alignment: .leading)
} }
} }
@@ -135,7 +135,7 @@ struct ProgramGuide: View {
.clipped() .clipped()
} }
.frame(height: axisHeight) .frame(height: axisHeight)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
} }
/// Was auf diesem Sender gerade läuft. /// Was auf diesem Sender gerade läuft.
@@ -147,20 +147,20 @@ struct ProgramGuide: View {
VStack(spacing: 0) { VStack(spacing: 0) {
ForEach(channels) { channel in ForEach(channels) { channel in
Button { onTune(channel, running(on: channel)) } label: { Button { onTune(channel, running(on: channel)) } label: {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
FlixyArtwork(url: artwork(channel), aspectRatio: 1, fallbackSymbol: "tv") HaloArtwork(url: artwork(channel), aspectRatio: 1, fallbackSymbol: "tv")
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
.clipShape(.rect(cornerRadius: 4)) .clipShape(.rect(cornerRadius: 4))
VStack(alignment: .leading, spacing: 1) { VStack(alignment: .leading, spacing: 1) {
Text(channel.title) Text(channel.title)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(2) .lineLimit(2)
if let number = channel.number { if let number = channel.number {
Text(number) Text(number)
.font(.system(.caption2, design: .monospaced)) .font(.system(.caption2, design: .monospaced))
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
} }
@@ -170,7 +170,7 @@ struct ProgramGuide: View {
ProgressView().controlSize(.mini) ProgressView().controlSize(.mini)
} }
} }
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.frame(width: channelWidth, height: rowHeight, alignment: .leading) .frame(width: channelWidth, height: rowHeight, alignment: .leading)
.overlay(alignment: .bottom) { rowSeparator } .overlay(alignment: .bottom) { rowSeparator }
.contentShape(.rect) .contentShape(.rect)
@@ -179,7 +179,7 @@ struct ProgramGuide: View {
} }
} }
.frame(width: channelWidth) .frame(width: channelWidth)
.background(Color.flixyElevated.opacity(0.4)) .background(Color.haloElevated.opacity(0.4))
} }
private var lanes: some View { private var lanes: some View {
@@ -217,9 +217,9 @@ struct ProgramGuide: View {
return ZStack(alignment: .topLeading) { return ZStack(alignment: .topLeading) {
if entries.isEmpty { if entries.isEmpty {
Text("liveTV.noProgram") Text("liveTV.noProgram")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.frame(height: rowHeight, alignment: .leading) .frame(height: rowHeight, alignment: .leading)
} }
@@ -240,7 +240,7 @@ struct ProgramGuide: View {
/// Zeile, die man antippt, gehört dann zum Nachbarsender. /// Zeile, die man antippt, gehört dann zum Nachbarsender.
private var rowSeparator: some View { private var rowSeparator: some View {
Rectangle() Rectangle()
.fill(Color.flixySeparator) .fill(Color.haloSeparator)
.frame(height: 1) .frame(height: 1)
} }
@@ -250,21 +250,21 @@ struct ProgramGuide: View {
return Button { selection = GuideSelection(channel: channel, program: program) } label: { return Button { selection = GuideSelection(channel: channel, program: program) } label: {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(program.title) Text(program.title)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
Text(program.startsAt.formatted(date: .omitted, time: .shortened)) Text(program.startsAt.formatted(date: .omitted, time: .shortened))
.font(.system(.caption2, design: .rounded)) .font(.system(.caption2, design: .rounded))
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.padding(.vertical, 6) .padding(.vertical, 6)
.frame(width: layout.width(from: program.startsAt, to: program.endsAt) - 2, height: rowHeight - 2, alignment: .topLeading) .frame(width: layout.width(from: program.startsAt, to: program.endsAt) - 2, height: rowHeight - 2, alignment: .topLeading)
.background( .background(
running ? accent.opacity(0.28) : Color.flixyElevated, running ? accent.opacity(0.28) : Color.haloElevated,
in: .rect(cornerRadius: FlixyRadius.control) in: .rect(cornerRadius: HaloRadius.control)
) )
.overlay(alignment: .bottomLeading) { .overlay(alignment: .bottomLeading) {
// Der Balken zeigt, wie weit die laufende Sendung ist — beim // Der Balken zeigt, wie weit die laufende Sendung ist — beim
@@ -299,45 +299,45 @@ private struct ProgramDetailSheet: View {
let onRecord: (() -> Void)? let onRecord: (() -> Void)?
let onRecordSeries: (() -> Void)? let onRecordSeries: (() -> Void)?
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
FlixyArtwork(url: artworkURL, aspectRatio: 1, fallbackSymbol: "tv") HaloArtwork(url: artworkURL, aspectRatio: 1, fallbackSymbol: "tv")
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.clipShape(.rect(cornerRadius: 6)) .clipShape(.rect(cornerRadius: 6))
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(channel.title) Text(channel.title)
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Text("liveTV.timeRange \(program.startsAt.formatted(date: .omitted, time: .shortened)) \(program.endsAt.formatted(date: .omitted, time: .shortened))") Text("liveTV.timeRange \(program.startsAt.formatted(date: .omitted, time: .shortened)) \(program.endsAt.formatted(date: .omitted, time: .shortened))")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
Spacer() Spacer()
} }
Text(program.title) Text(program.title)
.font(.flixyRowHeader) .font(.haloRowHeader)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
if let subtitle = program.subtitle { if let subtitle = program.subtitle {
Text(subtitle) Text(subtitle)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
if isRunning { if isRunning {
Button { onTune(); dismiss() } label: { Button { onTune(); dismiss() } label: {
Label("liveTV.tune", systemImage: "play.fill") Label("liveTV.tune", systemImage: "play.fill")
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
@@ -348,15 +348,15 @@ private struct ProgramDetailSheet: View {
// ohne Aufnahmen fehlt hier schlicht alles. // ohne Aufnahmen fehlt hier schlicht alles.
if program.seriesTimerID != nil { if program.seriesTimerID != nil {
Label("liveTV.alreadyScheduled", systemImage: "checkmark.circle") Label("liveTV.alreadyScheduled", systemImage: "checkmark.circle")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} else if onRecord != nil || onRecordSeries != nil { } else if onRecord != nil || onRecordSeries != nil {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
if let onRecord { if let onRecord {
Button { onRecord(); dismiss() } label: { Button { onRecord(); dismiss() } label: {
Label("liveTV.record", systemImage: "record.circle") Label("liveTV.record", systemImage: "record.circle")
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
@@ -364,7 +364,7 @@ private struct ProgramDetailSheet: View {
Button { onRecordSeries(); dismiss() } label: { Button { onRecordSeries(); dismiss() } label: {
Label("liveTV.recordSeries", systemImage: "rectangle.stack.badge.record") Label("liveTV.recordSeries", systemImage: "rectangle.stack.badge.record")
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
@@ -373,20 +373,20 @@ private struct ProgramDetailSheet: View {
if let summary = program.summary, !summary.isEmpty { if let summary = program.summary, !summary.isEmpty {
Text(summary) Text(summary)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
.padding(FlixySpacing.screenMargin) .padding(HaloSpacing.screenMargin)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button("action.close") { dismiss() } Button("action.close") { dismiss() }
} }
} }
} }
.flixyResizableSheet() .haloResizableSheet()
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Aufnahmen und was noch vorgemerkt ist. /// Aufnahmen und was noch vorgemerkt ist.
@@ -12,7 +12,7 @@ import MediaKit
/// kommt noch" sind dieselbe Frage in zwei Zeitformen, und wer die eine /// kommt noch" sind dieselbe Frage in zwei Zeitformen, und wer die eine
/// stellt, stellt meist gleich die andere mit. /// stellt, stellt meist gleich die andere mit.
struct RecordingsView: View { struct RecordingsView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@State private var recordings: [LiveRecording] = [] @State private var recordings: [LiveRecording] = []
@State private var timers: [LiveTimer] = [] @State private var timers: [LiveTimer] = []
@@ -47,7 +47,7 @@ struct RecordingsView: View {
list list
} }
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.task { await load() } .task { await load() }
.refreshable { await load() } .refreshable { await load() }
.confirmationDialog( .confirmationDialog(
@@ -90,7 +90,7 @@ struct RecordingsView: View {
} }
} }
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} header: { } header: {
Text("liveTV.recordings") Text("liveTV.recordings")
} }
@@ -118,23 +118,23 @@ struct RecordingsView: View {
} }
} }
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} header: { } header: {
Text("liveTV.scheduled") Text("liveTV.scheduled")
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} }
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
// MARK: - Zeilen // MARK: - Zeilen
private func row(_ recording: LiveRecording) -> some View { private func row(_ recording: LiveRecording) -> some View {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
FlixyArtwork( HaloArtwork(
url: session.backend?.artworkURL(path: recording.artworkPath, width: 200, height: 120), url: session.backend?.artworkURL(path: recording.artworkPath, width: 200, height: 120),
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
fallbackSymbol: "recordingtape", fallbackSymbol: "recordingtape",
@@ -145,23 +145,23 @@ struct RecordingsView: View {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(recording.title) Text(recording.title)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(1) .lineLimit(1)
HStack(spacing: FlixySpacing.tight) { HStack(spacing: HaloSpacing.tight) {
if recording.isInProgress { if recording.isInProgress {
// Eine laufende Aufnahme ist keine fertige: sie wächst // Eine laufende Aufnahme ist keine fertige: sie wächst
// noch, und wer sie jetzt öffnet, sieht ein Ende, das // noch, und wer sie jetzt öffnet, sieht ein Ende, das
// nicht das Ende ist. // nicht das Ende ist.
Label("liveTV.recording.inProgress", systemImage: "record.circle") Label("liveTV.recording.inProgress", systemImage: "record.circle")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyDestructive) .foregroundStyle(Color.haloDestructive)
} }
if let subtitle = subtitle(for: recording) { if let subtitle = subtitle(for: recording) {
Text(subtitle) Text(subtitle)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.lineLimit(1) .lineLimit(1)
} }
} }
@@ -175,29 +175,29 @@ struct RecordingsView: View {
private func timerRow(_ timer: LiveTimer) -> some View { private func timerRow(_ timer: LiveTimer) -> some View {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(timer.title) Text(timer.title)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(1) .lineLimit(1)
if let subtitle = subtitle(channel: timer.channelName, start: timer.startsAt) { if let subtitle = subtitle(channel: timer.channelName, start: timer.startsAt) {
Text(subtitle) Text(subtitle)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
} }
private func seriesRow(_ timer: LiveSeriesTimer) -> some View { private func seriesRow(_ timer: LiveSeriesTimer) -> some View {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
Image(systemName: "rectangle.stack.badge.record") Image(systemName: "rectangle.stack.badge.record")
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(timer.name) Text(timer.name)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(1) .lineLimit(1)
Text(timer.recordsAnyChannel ? "liveTV.anyChannel" : "liveTV.series") Text(timer.recordsAnyChannel ? "liveTV.anyChannel" : "liveTV.series")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
} }
@@ -52,7 +52,7 @@ extension LoadState {
/// Übersetzt Fehler in Sätze, die man einem Menschen zeigen kann. /// Übersetzt Fehler in Sätze, die man einem Menschen zeigen kann.
/// ///
/// **Eine** Stelle. Es gab davon zwei — hier und in `FlixySession` —, und sie /// **Eine** Stelle. Es gab davon zwei — hier und in `HaloSession` —, und sie
/// waren bereits auseinandergelaufen: derselbe Fehler bekam je nach /// waren bereits auseinandergelaufen: derselbe Fehler bekam je nach
/// Bildschirm eine andere Erklärung. /// Bildschirm eine andere Erklärung.
enum ErrorText { enum ErrorText {
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Der erste Buchstabe auf einer Scheibe. /// Der erste Buchstabe auf einer Scheibe.
/// ///
@@ -16,7 +16,7 @@ struct AccountAvatar: View {
let name: String let name: String
var diameter: CGFloat = 26 var diameter: CGFloat = 26
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
Text(name.prefix(1).uppercased()) Text(name.prefix(1).uppercased())
@@ -25,7 +25,7 @@ struct AccountAvatar: View {
// Kleinschrift auf einer grossen Scheibe sieht aus wie ein // Kleinschrift auf einer grossen Scheibe sieht aus wie ein
// Fehler. // Fehler.
.font(.system(size: diameter * 0.46, weight: .semibold)) .font(.system(size: diameter * 0.46, weight: .semibold))
.foregroundStyle(Color.flixyCanvas) .foregroundStyle(Color.haloCanvas)
.frame(width: diameter, height: diameter) .frame(width: diameter, height: diameter)
.background(accent, in: .circle) .background(accent, in: .circle)
} }
@@ -38,7 +38,7 @@ extension EnvironmentValues {
/// Blatt gehört der Wurzel — nur sie überlebt den Wechsel des Bereichs. /// Blatt gehört der Wurzel — nur sie überlebt den Wechsel des Bereichs.
/// Jeder Bildschirm, der den Knopf zeigt, müsste den Weg dorthin sonst /// Jeder Bildschirm, der den Knopf zeigt, müsste den Weg dorthin sonst
/// als Argument durchgereicht bekommen. /// als Argument durchgereicht bekommen.
@Entry var flixyOpenSettings: () -> Void = {} @Entry var haloOpenSettings: () -> Void = {}
} }
/// Das Monogramm als Knopf für eine Werkzeugleiste. /// Das Monogramm als Knopf für eine Werkzeugleiste.
@@ -48,8 +48,8 @@ extension EnvironmentValues {
/// damit **vor** Suche, Filter und Ansicht statt dahinter. Wo in der Reihe es /// damit **vor** Suche, Filter und Ansicht statt dahinter. Wo in der Reihe es
/// steht, kann nur der entscheiden, der die Reihe schreibt. /// steht, kann nur der entscheiden, der die Reihe schreibt.
struct AccountToolbarButton: View { struct AccountToolbarButton: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.flixyOpenSettings) private var openSettings @Environment(\.haloOpenSettings) private var openSettings
var body: some View { var body: some View {
Button(action: openSettings) { Button(action: openSettings) {
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Wer angemeldet ist — unten in der Seitenleiste. /// Wer angemeldet ist — unten in der Seitenleiste.
/// ///
@@ -22,7 +22,7 @@ import FlixyDesign
struct AccountBar: View { struct AccountBar: View {
@Binding var selection: SidebarDestination @Binding var selection: SidebarDestination
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@State private var isExpanded = false @State private var isExpanded = false
@State private var showsSignOutConfirmation = false @State private var showsSignOutConfirmation = false
@@ -55,7 +55,7 @@ struct AccountBar: View {
} }
} }
.frame(minWidth: 240) .frame(minWidth: 240)
.background(Color.flixySurface) .background(Color.haloSurface)
// Auf dem iPhone gäbe es diese Leiste gar nicht; die Angabe // Auf dem iPhone gäbe es diese Leiste gar nicht; die Angabe
// steht trotzdem da, damit aus dem Popover dort kein // steht trotzdem da, damit aus dem Popover dort kein
// bildschirmfüllendes Sheet wird. // bildschirmfüllendes Sheet wird.
@@ -77,19 +77,19 @@ struct AccountBar: View {
Button { Button {
isExpanded.toggle() isExpanded.toggle()
} label: { } label: {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
AccountAvatar(name: title) AccountAvatar(name: title)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(title) Text(title)
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.lineLimit(1) .lineLimit(1)
if let subtitle { if let subtitle {
Text(subtitle) Text(subtitle)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.lineLimit(1) .lineLimit(1)
} }
} }
@@ -102,10 +102,10 @@ struct AccountBar: View {
// Zeigt nach oben: dorthin geht die Karte auf. // Zeigt nach oben: dorthin geht die Karte auf.
Image(systemName: "chevron.up") Image(systemName: "chevron.up")
.font(.caption2.weight(.semibold)) .font(.caption2.weight(.semibold))
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.contentShape(.rect) .contentShape(.rect)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -121,18 +121,18 @@ struct AccountBar: View {
) -> some View { ) -> some View {
Button(action: perform) { Button(action: perform) {
Label(title, systemImage: symbol) Label(title, systemImage: symbol)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(isDestructive ? Color.flixyDestructive : Color.flixyTextPrimary) .foregroundStyle(isDestructive ? Color.haloDestructive : Color.haloTextPrimary)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
.contentShape(.rect) .contentShape(.rect)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
private var separator: some View { private var separator: some View {
Divider().overlay(Color.flixySeparator) Divider().overlay(Color.haloSeparator)
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Wo die Navigation sitzt. /// Wo die Navigation sitzt.
@@ -49,7 +49,7 @@ struct NavigationGroup: Identifiable {
/// mit Kontur; Glas bringt seine Kante selbst mit — sie hellt auf, wo Licht /// mit Kontur; Glas bringt seine Kante selbst mit — sie hellt auf, wo Licht
/// fällt, und verschwindet, wo keins ist. Das ist der Unterschied zwischen /// fällt, und verschwindet, wo keins ist. Das ist der Unterschied zwischen
/// „umrandet" und „aus Glas". /// „umrandet" und „aus Glas".
struct FlixySidebar: View { struct HaloSidebar: View {
/// Alle Einträge, **ungefiltert**. Was davon zu sehen ist, entscheidet /// Alle Einträge, **ungefiltert**. Was davon zu sehen ist, entscheidet
/// `layout` — beim Bearbeiten müssen auch die ausgeblendeten dastehen. /// `layout` — beim Bearbeiten müssen auch die ausgeblendeten dastehen.
let groups: [NavigationGroup] let groups: [NavigationGroup]
@@ -57,7 +57,7 @@ struct FlixySidebar: View {
@Binding var placement: NavigationPlacement @Binding var placement: NavigationPlacement
let layout: NavigationLayoutSettings let layout: NavigationLayoutSettings
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
/// Welche Gruppen eingeklappt sind. Nach Kennung, nicht nach Stellung: /// Welche Gruppen eingeklappt sind. Nach Kennung, nicht nach Stellung:
/// kommt eine Mediathek dazu, soll nicht plötzlich die falsche zuklappen. /// kommt eine Mediathek dazu, soll nicht plötzlich die falsche zuklappen.
@State private var collapsed: Set<String> = [] @State private var collapsed: Set<String> = []
@@ -80,7 +80,7 @@ struct FlixySidebar: View {
static func surface(in shape: some InsettableShape) -> some View { static func surface(in shape: some InsettableShape) -> some View {
shape shape
.fill(.ultraThinMaterial) .fill(.ultraThinMaterial)
.overlay { shape.fill(Color.flixyCanvas.opacity(0.55)) } .overlay { shape.fill(Color.haloCanvas.opacity(0.55)) }
.overlay { .overlay {
shape.strokeBorder( shape.strokeBorder(
LinearGradient( LinearGradient(
@@ -134,10 +134,10 @@ struct FlixySidebar: View {
// Ziehen zum Umsortieren mit, und die scrollt von sich aus. // Ziehen zum Umsortieren mit, und die scrollt von sich aus.
if isEditing { if isEditing {
Text("sidebar.editHint") Text("sidebar.editHint")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.bottom, FlixySpacing.snug) .padding(.bottom, HaloSpacing.snug)
List { List {
ForEach(groups) { group in ForEach(groups) { group in
@@ -149,8 +149,8 @@ struct FlixySidebar: View {
} header: { } header: {
if let title = group.title { if let title = group.title {
Text(title) Text(title)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
} }
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
@@ -162,13 +162,13 @@ struct FlixySidebar: View {
.environment(\.editMode, .constant(.active)) .environment(\.editMode, .constant(.active))
} else { } else {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
ForEach(groups) { group in ForEach(groups) { group in
section(visible(group)) section(visible(group))
} }
} }
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.padding(.bottom, FlixySpacing.regular) .padding(.bottom, HaloSpacing.regular)
} }
.scrollBounceBehavior(.basedOnSize) .scrollBounceBehavior(.basedOnSize)
.scrollIndicators(.never) .scrollIndicators(.never)
@@ -176,14 +176,14 @@ struct FlixySidebar: View {
Spacer(minLength: 0) Spacer(minLength: 0)
Divider().overlay(Color.flixySeparator) Divider().overlay(Color.haloSeparator)
AccountBar(selection: $selection) AccountBar(selection: $selection)
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} }
.frame(width: Self.width) .frame(width: Self.width)
.background { Self.surface(in: .rect(cornerRadius: FlixyRadius.panel, style: .continuous)) } .background { Self.surface(in: .rect(cornerRadius: HaloRadius.panel, style: .continuous)) }
.clipShape(.rect(cornerRadius: FlixyRadius.panel, style: .continuous)) .clipShape(.rect(cornerRadius: HaloRadius.panel, style: .continuous))
.shadow(color: .black.opacity(0.45), radius: 22, y: 8) .shadow(color: .black.opacity(0.45), radius: 22, y: 8)
// **Kein** Abstand nach rechts: dort schließt der Inhalt unmittelbar // **Kein** Abstand nach rechts: dort schließt der Inhalt unmittelbar
// an. Ein Abstand dorthin war der schwarze Spalt zwischen Aufmacher // an. Ein Abstand dorthin war der schwarze Spalt zwischen Aufmacher
@@ -194,18 +194,18 @@ struct FlixySidebar: View {
} }
private var header: some View { private var header: some View {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
Button { Button {
withAnimation(.snappy(duration: 0.25)) { isEditing.toggle() } withAnimation(.snappy(duration: 0.25)) { isEditing.toggle() }
} label: { } label: {
Text(isEditing ? "action.done" : "sidebar.edit") Text(isEditing ? "action.done" : "sidebar.edit")
.font(.flixyCaption) .font(.haloCaption)
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.padding(.vertical, FlixySpacing.tight) .padding(.vertical, HaloSpacing.tight)
.contentShape(Capsule()) .contentShape(Capsule())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundStyle(isEditing ? accent : Color.flixyTextSecondary) .foregroundStyle(isEditing ? accent : Color.haloTextSecondary)
Spacer(minLength: 0) Spacer(minLength: 0)
@@ -214,21 +214,21 @@ struct FlixySidebar: View {
placement = .top placement = .top
} label: { } label: {
Image(systemName: "rectangle.topthird.inset.filled") Image(systemName: "rectangle.topthird.inset.filled")
.font(.flixyRowHeader) .font(.haloRowHeader)
.padding(FlixySpacing.tight) .padding(HaloSpacing.tight)
// Ohne eigene Klickfläche reicht das Ziel nur so weit // Ohne eigene Klickfläche reicht das Ziel nur so weit
// wie das Zeichen selbst — auf dem Mac trifft man das // wie das Zeichen selbst — auf dem Mac trifft man das
// mit dem Zeiger kaum. // mit dem Zeiger kaum.
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.accessibilityLabel(Text("sidebar.moveToTop")) .accessibilityLabel(Text("sidebar.moveToTop"))
} }
} }
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.top, FlixySpacing.regular) .padding(.top, HaloSpacing.regular)
.padding(.bottom, FlixySpacing.snug) .padding(.bottom, HaloSpacing.snug)
} }
/// Die Einträge einer Gruppe beim Bearbeiten: alle, in der gespeicherten /// Die Einträge einer Gruppe beim Bearbeiten: alle, in der gespeicherten
@@ -271,16 +271,16 @@ struct FlixySidebar: View {
} label: { } label: {
HStack { HStack {
Text(title) Text(title)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
Spacer() Spacer()
Image(systemName: "chevron.down") Image(systemName: "chevron.down")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.rotationEffect(.degrees(collapsed.contains(group.id) ? -90 : 0)) .rotationEffect(.degrees(collapsed.contains(group.id) ? -90 : 0))
} }
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.top, FlixySpacing.snug) .padding(.top, HaloSpacing.snug)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@@ -301,12 +301,12 @@ struct FlixySidebar: View {
private func editRow(_ entry: NavigationEntry) -> some View { private func editRow(_ entry: NavigationEntry) -> some View {
let isHidden = layout.isHidden(entry.arrangementID) let isHidden = layout.isHidden(entry.arrangementID)
return HStack(spacing: FlixySpacing.snug) { return HStack(spacing: HaloSpacing.snug) {
Button { Button {
layout.setHidden(!isHidden, for: entry.arrangementID) layout.setHidden(!isHidden, for: entry.arrangementID)
} label: { } label: {
Image(systemName: isHidden ? "circle" : "checkmark.circle.fill") Image(systemName: isHidden ? "circle" : "checkmark.circle.fill")
.foregroundStyle(isHidden ? Color.flixyTextMuted : accent) .foregroundStyle(isHidden ? Color.haloTextMuted : accent)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -318,8 +318,8 @@ struct FlixySidebar: View {
.lineLimit(1) .lineLimit(1)
Spacer(minLength: 0) Spacer(minLength: 0)
} }
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(isHidden ? Color.flixyTextMuted : Color.flixyTextPrimary) .foregroundStyle(isHidden ? Color.haloTextMuted : Color.haloTextPrimary)
} }
private func row(_ entry: NavigationEntry) -> some View { private func row(_ entry: NavigationEntry) -> some View {
@@ -328,23 +328,23 @@ struct FlixySidebar: View {
return Button { return Button {
selection = entry.destination selection = entry.destination
} label: { } label: {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
Image(systemName: entry.symbol) Image(systemName: entry.symbol)
.font(.flixyBody) .font(.haloBody)
.frame(width: 22, alignment: .center) .frame(width: 22, alignment: .center)
entry.title entry.title
.font(.flixyBody) .font(.haloBody)
// Ein langer Mediathekenname darf die Leiste nicht // Ein langer Mediathekenname darf die Leiste nicht
// breiter machen; sie hat eine feste Breite. // breiter machen; sie hat eine feste Breite.
.lineLimit(1) .lineLimit(1)
Spacer(minLength: 0) Spacer(minLength: 0)
} }
.foregroundStyle(isSelected ? accent : Color.flixyTextPrimary) .foregroundStyle(isSelected ? accent : Color.haloTextPrimary)
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.background { .background {
if isSelected { if isSelected {
Capsule().fill(Color.flixyElevated) Capsule().fill(Color.haloElevated)
} }
} }
.contentShape(Capsule()) .contentShape(Capsule())
@@ -360,29 +360,29 @@ struct FlixySidebar: View {
/// Mediatheken stehen hier in einem Menü statt einzeln: oben ist der Platz /// Mediatheken stehen hier in einem Menü statt einzeln: oben ist der Platz
/// waagerecht begrenzt, und eine Reihe, die bei acht Mediatheken über den Rand /// waagerecht begrenzt, und eine Reihe, die bei acht Mediatheken über den Rand
/// hinauswächst, wäre schlechter als ein Menü. /// hinauswächst, wäre schlechter als ein Menü.
struct FlixyTopBar: View { struct HaloTopBar: View {
let groups: [NavigationGroup] let groups: [NavigationGroup]
@Binding var selection: SidebarDestination @Binding var selection: SidebarDestination
@Binding var placement: NavigationPlacement @Binding var placement: NavigationPlacement
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
HStack(spacing: FlixySpacing.tight) { HStack(spacing: HaloSpacing.tight) {
Button { Button {
placement = .sidebar placement = .sidebar
} label: { } label: {
Image(systemName: "sidebar.leading") Image(systemName: "sidebar.leading")
.font(.flixyBody) .font(.haloBody)
.padding(.horizontal, FlixySpacing.regular) .padding(.horizontal, HaloSpacing.regular)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
// Ohne eigene Klickfläche reicht das Ziel nur so weit wie // Ohne eigene Klickfläche reicht das Ziel nur so weit wie
// das Zeichen; auf dem Mac war der Umschalter damit // das Zeichen; auf dem Mac war der Umschalter damit
// praktisch nicht zu treffen. // praktisch nicht zu treffen.
.contentShape(Capsule()) .contentShape(Capsule())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.accessibilityLabel(Text("sidebar.moveToSide")) .accessibilityLabel(Text("sidebar.moveToSide"))
ForEach(groups) { group in ForEach(groups) { group in
@@ -395,12 +395,12 @@ struct FlixyTopBar: View {
} }
} }
} }
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.background { FlixySidebar.surface(in: .capsule) } .background { HaloSidebar.surface(in: .capsule) }
.clipShape(.capsule) .clipShape(.capsule)
.shadow(color: .black.opacity(0.45), radius: 18, y: 6) .shadow(color: .black.opacity(0.45), radius: 18, y: 6)
.padding(.top, FlixySpacing.snug) .padding(.top, HaloSpacing.snug)
} }
private func pill(_ entry: NavigationEntry) -> some View { private func pill(_ entry: NavigationEntry) -> some View {
@@ -410,13 +410,13 @@ struct FlixyTopBar: View {
selection = entry.destination selection = entry.destination
} label: { } label: {
entry.title entry.title
.font(.flixyBody) .font(.haloBody)
.lineLimit(1) .lineLimit(1)
.foregroundStyle(isSelected ? accent : Color.flixyTextPrimary) .foregroundStyle(isSelected ? accent : Color.haloTextPrimary)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.background { .background {
if isSelected { Capsule().fill(Color.flixyElevated) } if isSelected { Capsule().fill(Color.haloElevated) }
} }
.contentShape(Capsule()) .contentShape(Capsule())
} }
@@ -433,21 +433,21 @@ struct FlixyTopBar: View {
} }
} }
} label: { } label: {
HStack(spacing: FlixySpacing.tight) { HStack(spacing: HaloSpacing.tight) {
if let title = group.title { if let title = group.title {
// Ohne das wurde aus „Mediatheken" ein „Mediathe…". // Ohne das wurde aus „Mediatheken" ein „Mediathe…".
// Die Leiste ist so breit wie ihr Inhalt; sie hat keinen // Die Leiste ist so breit wie ihr Inhalt; sie hat keinen
// Grund, an einem Wort zu sparen. // Grund, an einem Wort zu sparen.
Text(title).fixedSize() Text(title).fixedSize()
} }
Image(systemName: "chevron.down").font(.flixyCaption) Image(systemName: "chevron.down").font(.haloCaption)
} }
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(containsSelection ? accent : Color.flixyTextPrimary) .foregroundStyle(containsSelection ? accent : Color.haloTextPrimary)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.background { .background {
if containsSelection { Capsule().fill(Color.flixyElevated) } if containsSelection { Capsule().fill(Color.haloElevated) }
} }
.contentShape(Capsule()) .contentShape(Capsule())
} }
@@ -16,7 +16,7 @@ import MediaKit
struct MediaStack<Content: View>: View { struct MediaStack<Content: View>: View {
@ViewBuilder let content: () -> Content @ViewBuilder let content: () -> Content
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@State private var path = NavigationPath() @State private var path = NavigationPath()
var body: some View { var body: some View {
@@ -31,7 +31,7 @@ struct MediaStack<Content: View>: View {
.navigationDestination(for: LiveTVRoute.self) { _ in LiveTVView() } .navigationDestination(for: LiveTVRoute.self) { _ in LiveTVView() }
.navigationDestination(for: SearchRoute.self) { _ in SearchView() } .navigationDestination(for: SearchRoute.self) { _ in SearchView() }
} }
.environment(\.flixyPush) { ziel in path.append(ziel) } .environment(\.haloPush) { ziel in path.append(ziel) }
} }
} }
@@ -45,5 +45,5 @@ extension EnvironmentValues {
/// ///
/// Die Voreinstellung tut nichts: ausserhalb eines Stapels gibt es nichts /// Die Voreinstellung tut nichts: ausserhalb eines Stapels gibt es nichts
/// zu schieben, und ein Absturz wäre die schlechtere Antwort. /// zu schieben, und ein Absturz wäre die schlechtere Antwort.
@Entry var flixyPush: (any Hashable) -> Void = { _ in } @Entry var haloPush: (any Hashable) -> Void = { _ in }
} }
@@ -1,8 +1,8 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
import FlixyDownloads import HaloDownloads
import FlixyPlayback import HaloPlayback
/// Die Wurzelansicht. /// Die Wurzelansicht.
/// ///
@@ -10,8 +10,8 @@ import FlixyPlayback
/// Welche Form die Navigation danach annimmt — Seitenleiste, Leiste oben, /// Welche Form die Navigation danach annimmt — Seitenleiste, Leiste oben,
/// Leiste unten —, entscheidet das System und der Nutzer, nicht diese Datei. /// Leiste unten —, entscheidet das System und der Nutzer, nicht diese Datei.
struct RootView: View { struct RootView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(FlixyTheme.self) private var theme @Environment(HaloTheme.self) private var theme
@Environment(PlaybackCoordinator.self) private var playback @Environment(PlaybackCoordinator.self) private var playback
@Environment(PlaybackSettings.self) private var settings @Environment(PlaybackSettings.self) private var settings
@Environment(LanguagePreferences.self) private var languages @Environment(LanguagePreferences.self) private var languages
@@ -35,13 +35,13 @@ struct RootView: View {
/// Reihenfolge, Ein- und Ausblenden, und ob ein Eintrag in der Leiste oben /// Reihenfolge, Ein- und Ausblenden, und ob ein Eintrag in der Leiste oben
/// liegt: das System schreibt es hier hinein, wir geben ihm nur einen Ort /// liegt: das System schreibt es hier hinein, wir geben ihm nur einen Ort
/// zum Aufbewahren. /// zum Aufbewahren.
@AppStorage("flixy.navigation.customization") @AppStorage("halo.navigation.customization")
private var customization = TabViewCustomization() private var customization = TabViewCustomization()
/// Ob die Navigation an der Seite oder oben sitzt — gespeichert. /// Ob die Navigation an der Seite oder oben sitzt — gespeichert.
/// ///
/// Die Wahl gehört dem Nutzer und muss den Neustart überleben. /// Die Wahl gehört dem Nutzer und muss den Neustart überleben.
@AppStorage("flixy.navigation.placement") @AppStorage("halo.navigation.placement")
private var storedPlacement: NavigationPlacement = .sidebar private var storedPlacement: NavigationPlacement = .sidebar
/// Dieselbe Wahl, aber als Zustand dieser Ansicht. /// Dieselbe Wahl, aber als Zustand dieser Ansicht.
@@ -85,7 +85,7 @@ struct RootView: View {
case .launching: case .launching:
ProgressView() ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
case .signedOut: case .signedOut:
SignInView() SignInView()
@@ -104,7 +104,7 @@ struct RootView: View {
library library
} }
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.environment(topChrome) .environment(topChrome)
// **Aus dem Fenster, nicht aus dem Layout.** // **Aus dem Fenster, nicht aus dem Layout.**
// //
@@ -121,7 +121,7 @@ struct RootView: View {
// Durchgang. // Durchgang.
// //
// **Nicht** die Ursache des Players, der oben und unten hinausging: // **Nicht** die Ursache des Players, der oben und unten hinausging:
// das war die Seitenleiste selbst, siehe `FlixySidebar`. Diese Zeile // das war die Seitenleiste selbst, siehe `HaloSidebar`. Diese Zeile
// spart eine Layoutrunde, mehr nicht. // spart eine Layoutrunde, mehr nicht.
.onGeometryChange(for: CGSize.self) { proxy in .onGeometryChange(for: CGSize.self) { proxy in
proxy.size proxy.size
@@ -129,7 +129,7 @@ struct RootView: View {
.onAppear { statusBarHeight = ScreenSafeArea.current.top } .onAppear { statusBarHeight = ScreenSafeArea.current.top }
// Hier statt in der Scene: eine View wertet Beobachtungsänderungen // Hier statt in der Scene: eine View wertet Beobachtungsänderungen
// zuverlässig neu aus, ein Scene-Body nicht. // zuverlässig neu aus, ein Scene-Body nicht.
.flixyAccent(theme) .haloAccent(theme)
.tint(theme.accentColor) .tint(theme.accentColor)
// **Einer für alle Kacheln.** Einer je Kachel wäre bei einem // **Einer für alle Kacheln.** Einer je Kachel wäre bei einem
// Serverausfall ein Stapel aus zwanzig gleichen Meldungen. // Serverausfall ein Stapel aus zwanzig gleichen Meldungen.
@@ -174,7 +174,7 @@ struct RootView: View {
// ihn jemand von Hand antippte — auch dann, wenn das WLAN seit einer // ihn jemand von Hand antippte — auch dann, wenn das WLAN seit einer
// Stunde wieder stand. // Stunde wieder stand.
// //
// Hier und nicht im Koordinator: `FlixyDownloads` hängt bewusst nicht // Hier und nicht im Koordinator: `HaloDownloads` hängt bewusst nicht
// an `NetworkConditions`, es bekommt den Netzzustand als Wert gereicht // an `NetworkConditions`, es bekommt den Netzzustand als Wert gereicht
// (siehe `TransferHold`). Diese Ansicht kennt beide. // (siehe `TransferHold`). Diese Ansicht kennt beide.
.onChange(of: network.isReachable) { _, erreichbar in .onChange(of: network.isReachable) { _, erreichbar in
@@ -289,7 +289,7 @@ struct RootView: View {
.overlay(alignment: .topTrailing) { profileButton } .overlay(alignment: .topTrailing) { profileButton }
} }
} }
.customizationID("flixy.tab.home") .customizationID("halo.tab.home")
// Auf dem iPad jede Mediathek einzeln, auf dem iPhone alle // Auf dem iPad jede Mediathek einzeln, auf dem iPhone alle
// zusammen hinter einem Eintrag. Der Grund ist nicht Geschmack: // zusammen hinter einem Eintrag. Der Grund ist nicht Geschmack:
@@ -307,12 +307,12 @@ struct RootView: View {
} label: { } label: {
sidebarLabel(Text(library.title), symbol: library.kind.symbol) sidebarLabel(Text(library.title), symbol: library.kind.symbol)
} }
.customizationID("flixy.tab.library.\(library.id)") .customizationID("halo.tab.library.\(library.id)")
} }
} header: { } header: {
Text("sidebar.libraries") Text("sidebar.libraries")
} }
.customizationID("flixy.tab.libraries") .customizationID("halo.tab.libraries")
if session.supportsLiveTV { if session.supportsLiveTV {
Tab(value: SidebarDestination.liveTV) { Tab(value: SidebarDestination.liveTV) {
@@ -320,7 +320,7 @@ struct RootView: View {
} label: { } label: {
tabLabel("sidebar.liveTV", symbol: "dot.radiowaves.left.and.right") tabLabel("sidebar.liveTV", symbol: "dot.radiowaves.left.and.right")
} }
.customizationID("flixy.tab.liveTV") .customizationID("halo.tab.liveTV")
} }
} else { } else {
Tab(value: SidebarDestination.libraryHub) { Tab(value: SidebarDestination.libraryHub) {
@@ -328,7 +328,7 @@ struct RootView: View {
} label: { } label: {
tabLabel("sidebar.library", symbol: "books.vertical") tabLabel("sidebar.library", symbol: "books.vertical")
} }
.customizationID("flixy.tab.libraryHub") .customizationID("halo.tab.libraryHub")
} }
Tab(value: SidebarDestination.downloads) { Tab(value: SidebarDestination.downloads) {
@@ -336,7 +336,7 @@ struct RootView: View {
} label: { } label: {
tabLabel("sidebar.downloads", symbol: "arrow.down.circle") tabLabel("sidebar.downloads", symbol: "arrow.down.circle")
} }
.customizationID("flixy.tab.downloads") .customizationID("halo.tab.downloads")
// **Nicht auf dem iPhone.** Dort führt der Weg über das // **Nicht auf dem iPhone.** Dort führt der Weg über das
// Monogramm oben rechts — siehe `profileButton`. Einstellungen // Monogramm oben rechts — siehe `profileButton`. Einstellungen
@@ -349,7 +349,7 @@ struct RootView: View {
} label: { } label: {
tabLabel("sidebar.settings", symbol: "gearshape") tabLabel("sidebar.settings", symbol: "gearshape")
} }
.customizationID("flixy.tab.settings") .customizationID("halo.tab.settings")
} }
// Die Suche nur dort, wo Platz für sie ist. // Die Suche nur dort, wo Platz für sie ist.
@@ -362,7 +362,7 @@ struct RootView: View {
Tab(value: SidebarDestination.search, role: .search) { Tab(value: SidebarDestination.search, role: .search) {
mediaStack { SearchView() } mediaStack { SearchView() }
} }
.customizationID("flixy.tab.search") .customizationID("halo.tab.search")
} }
} }
.tabViewStyle(.sidebarAdaptable) .tabViewStyle(.sidebarAdaptable)
@@ -389,7 +389,7 @@ struct RootView: View {
if sizeClass == .regular { if sizeClass == .regular {
HStack(spacing: 0) { HStack(spacing: 0) {
if placement == .sidebar { if placement == .sidebar {
FlixySidebar( HaloSidebar(
groups: navigationGroups, groups: navigationGroups,
selection: $selection, selection: $selection,
placement: $placement.animation(Self.placementAnimation), placement: $placement.animation(Self.placementAnimation),
@@ -433,7 +433,7 @@ struct RootView: View {
.background { TabChromeSuppressor().frame(width: 0, height: 0) } .background { TabChromeSuppressor().frame(width: 0, height: 0) }
.overlay(alignment: .top) { .overlay(alignment: .top) {
if placement == .top { if placement == .top {
FlixyTopBar( HaloTopBar(
groups: navigationGroups.map(visible), groups: navigationGroups.map(visible),
selection: $selection, selection: $selection,
placement: $placement.animation(Self.placementAnimation) placement: $placement.animation(Self.placementAnimation)
@@ -473,7 +473,7 @@ struct RootView: View {
// Navigationsstapel um sich — die Medienziele aus `mediaStack` ruft // Navigationsstapel um sich — die Medienziele aus `mediaStack` ruft
// es nie auf. Und beim Schliessen steht man wieder dort, wo man war, // es nie auf. Und beim Schliessen steht man wieder dort, wo man war,
// statt in einem Bereich, den man erst wieder verlassen muss. // statt in einem Bereich, den man erst wieder verlassen muss.
.environment(\.flixyOpenSettings, { showsSettings = true }) .environment(\.haloOpenSettings, { showsSettings = true })
.sheet(isPresented: $showsSettings) { .sheet(isPresented: $showsSettings) {
NavigationStack { NavigationStack {
SettingsView() SettingsView()
@@ -481,7 +481,7 @@ struct RootView: View {
// jeder weiss das, und wer in einem Unterbildschirm steht, // jeder weiss das, und wer in einem Unterbildschirm steht,
// zieht damit gleich alles weg. Ein Knopf sagt es. // zieht damit gleich alles weg. Ein Knopf sagt es.
.toolbar { .toolbar {
ToolbarItem(placement: .flixyTrailing) { ToolbarItem(placement: .haloTrailing) {
Button("action.close") { showsSettings = false } Button("action.close") { showsSettings = false }
} }
} }
@@ -515,16 +515,16 @@ struct RootView: View {
showsSettings = true showsSettings = true
} label: { } label: {
AccountAvatar(name: session.accountName ?? session.serverName, diameter: 34) AccountAvatar(name: session.accountName ?? session.serverName, diameter: 34)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
// Damit er auch über einem hellen Aufmacher steht. // Damit er auch über einem hellen Aufmacher steht.
.background(.black.opacity(0.28), in: .circle) .background(.black.opacity(0.28), in: .circle)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(.trailing, FlixySpacing.regular) .padding(.trailing, HaloSpacing.regular)
// **Ohne die Statusleiste.** Auf dem iPhone sitzt der Baum innerhalb // **Ohne die Statusleiste.** Auf dem iPhone sitzt der Baum innerhalb
// des Sicherheitsrands; sie noch einmal dazuzurechnen schöbe den // des Sicherheitsrands; sie noch einmal dazuzurechnen schöbe den
// Knopf um ihre Höhe nach unten, mitten ins Bild. // Knopf um ihre Höhe nach unten, mitten ins Bild.
.padding(.top, FlixySpacing.snug) .padding(.top, HaloSpacing.snug)
.opacity(topChrome.isRevealed ? 1 : 0) .opacity(topChrome.isRevealed ? 1 : 0)
.allowsHitTesting(topChrome.isRevealed) .allowsHitTesting(topChrome.isRevealed)
.animation(.snappy(duration: 0.28), value: topChrome.isRevealed) .animation(.snappy(duration: 0.28), value: topChrome.isRevealed)
@@ -543,7 +543,7 @@ struct RootView: View {
/// schwebt nichts, worunter man greifen müsste. /// schwebt nichts, worunter man greifen müsste.
private var topInset: CGFloat { private var topInset: CGFloat {
guard sizeClass == .regular else { return 0 } guard sizeClass == .regular else { return 0 }
return statusBarHeight + (placement == .sidebar ? FlixySidebar.margin : Self.topBarHeight) return statusBarHeight + (placement == .sidebar ? HaloSidebar.margin : Self.topBarHeight)
} }
private var navigationGroups: [NavigationGroup] { private var navigationGroups: [NavigationGroup] {
@@ -40,7 +40,7 @@ extension View {
/// die nächstgelegene, und eine Reihe, die man seitwärts schiebt, steht /// die nächstgelegene, und eine Reihe, die man seitwärts schiebt, steht
/// immer bei null — die Insel käme beim Blättern zurück, obwohl die Seite /// immer bei null — die Insel käme beim Blättern zurück, obwohl die Seite
/// weit unten steht. /// weit unten steht.
func flixyRevealsTopChrome() -> some View { func haloRevealsTopChrome() -> some View {
modifier(RevealsTopChrome()) modifier(RevealsTopChrome())
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Ein noch nicht gebauter Bereich. /// Ein noch nicht gebauter Bereich.
/// ///
@@ -15,9 +15,9 @@ struct PlaceholderScreen: View {
} description: { } description: {
Text("placeholder.description") Text("placeholder.description")
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(destination.title) .navigationTitle(destination.title)
.flixyInlineTitle() .haloInlineTitle()
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Die schwebende Videokachel. /// Die schwebende Videokachel.
/// ///
@@ -51,7 +51,7 @@ struct MiniPlayer: View {
// ist als das Fenster, ließe sich nicht mehr wegziehen. // ist als das Fenster, ließe sich nicht mehr wegziehen.
let upper = max(160, min(bounds.width * 0.75, 560)) let upper = max(160, min(bounds.width * 0.75, 560))
let width = min(max(chosen * zoom, 140), upper) let width = min(max(chosen * zoom, 140), upper)
return CGSize(width: width, height: (width / FlixyAspectRatio.landscape).rounded()) return CGSize(width: width, height: (width / HaloAspectRatio.landscape).rounded())
} }
var body: some View { var body: some View {
@@ -105,10 +105,10 @@ struct MiniPlayer: View {
} }
} }
} }
.clipShape(.rect(cornerRadius: FlixyRadius.card)) .clipShape(.rect(cornerRadius: HaloRadius.card))
.overlay { .overlay {
RoundedRectangle(cornerRadius: FlixyRadius.card) RoundedRectangle(cornerRadius: HaloRadius.card)
.strokeBorder(Color.flixySeparator, lineWidth: 0.5) .strokeBorder(Color.haloSeparator, lineWidth: 0.5)
} }
.shadow(color: .black.opacity(0.5), radius: 12, y: 4) .shadow(color: .black.opacity(0.5), radius: 12, y: 4)
.contentShape(.rect) .contentShape(.rect)
@@ -158,9 +158,9 @@ struct MiniPlayer: View {
} }
} }
} }
.padding(FlixySpacing.tight) .padding(HaloSpacing.tight)
// Über dem Fortschrittsbalken, nicht darauf. // Über dem Fortschrittsbalken, nicht darauf.
.padding(.bottom, FlixySpacing.tight) .padding(.bottom, HaloSpacing.tight)
} }
/// Ein kleiner Knopf, der auf jedem Bild lesbar bleibt. /// Ein kleiner Knopf, der auf jedem Bild lesbar bleibt.
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Der Regler unter dem Film — mit dem, was schon geladen ist. /// Der Regler unter dem Film — mit dem, was schon geladen ist.
/// ///
@@ -34,7 +34,7 @@ struct PlaybackScrubber: View {
/// Bei jeder Berührung — damit die Bedienung stehen bleibt. /// Bei jeder Berührung — damit die Bedienung stehen bleibt.
let onActivity: () -> Void let onActivity: () -> Void
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
private static let trackHeight: CGFloat = 5 private static let trackHeight: CGFloat = 5
private static let thumbSize: CGFloat = 13 private static let thumbSize: CGFloat = 13
@@ -2,8 +2,8 @@ import AVKit
import Foundation import Foundation
import MediaKit import MediaKit
import Observation import Observation
import FlixyDownloads import HaloDownloads
import FlixyPlayback import HaloPlayback
/// Führt einen Abspielwunsch aus. /// Führt einen Abspielwunsch aus.
/// ///
@@ -169,10 +169,10 @@ final class PlayerModel {
/// Wo das Bild aufhört. /// Wo das Bild aufhört.
/// ///
/// Liegt bei `FlixyPlayback`, weil beide Engines es brauchen. Der Name /// Liegt bei `HaloPlayback`, weil beide Engines es brauchen. Der Name
/// hier bleibt, damit die Ansicht weiter `PlayerModel.VideoFit` schreiben /// hier bleibt, damit die Ansicht weiter `PlayerModel.VideoFit` schreiben
/// kann. /// kann.
typealias VideoFit = FlixyPlayback.VideoFit typealias VideoFit = HaloPlayback.VideoFit
/// Bild-in-Bild hängt an genau dieser Ebene und wandert deshalb mit. /// Bild-in-Bild hängt an genau dieser Ebene und wandert deshalb mit.
/// ///
@@ -1,7 +1,7 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyDownloads import HaloDownloads
import FlixyPlayback import HaloPlayback
/// Der Vollbildplayer. /// Der Vollbildplayer.
struct PlayerView: View { struct PlayerView: View {
@@ -13,14 +13,14 @@ struct PlayerView: View {
/// Nutzers statt eine Nebenwirkung der Navigation. /// Nutzers statt eine Nebenwirkung der Navigation.
let model: PlayerModel let model: PlayerModel
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(PlaybackCoordinator.self) private var playback @Environment(PlaybackCoordinator.self) private var playback
@Environment(PlaybackSettings.self) private var settings @Environment(PlaybackSettings.self) private var settings
@Environment(SubtitleAppearance.self) private var subtitleAppearance @Environment(SubtitleAppearance.self) private var subtitleAppearance
@Environment(NetworkConditions.self) private var network @Environment(NetworkConditions.self) private var network
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(PlaybackDiagnostics.self) private var diagnostics @Environment(PlaybackDiagnostics.self) private var diagnostics
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
/// Woran sich Quer- und Hochformat unterscheiden lassen. /// Woran sich Quer- und Hochformat unterscheiden lassen.
/// ///
/// Auf jedem iPhone ist das Querformat senkrecht kompakt und das /// Auf jedem iPhone ist das Querformat senkrecht kompakt und das
@@ -72,7 +72,7 @@ struct PlayerView: View {
/// Knopf. /// Knopf.
@State private var isFullScreen = false @State private var isFullScreen = false
/// Ob Zeiger und Tastatur gelten. Einmal beim Start ermittelt. /// Ob Zeiger und Tastatur gelten. Einmal beim Start ermittelt.
private var isMac: Bool { FlixyPlatform.isMac } private var isMac: Bool { HaloPlatform.isMac }
/// Ob gedreht und mit zwei Fingern gezoomt werden kann. /// Ob gedreht und mit zwei Fingern gezoomt werden kann.
/// ///
/// Ein iPad dreht sich von selbst und braucht keinen Knopf dafür; der /// Ein iPad dreht sich von selbst und braucht keinen Knopf dafür; der
@@ -200,7 +200,7 @@ struct PlayerView: View {
} }
.opacity(zeigt ? 1 : 0) .opacity(zeigt ? 1 : 0)
.allowsHitTesting(zeigt) .allowsHitTesting(zeigt)
.padding(FlixySpacing.loose) .padding(HaloSpacing.loose)
// Siehe `controls(for:)`: die Kopfzeile lag sonst hinter // Siehe `controls(for:)`: die Kopfzeile lag sonst hinter
// der Dynamic Island, und der Zurück-Pfeil war tot. // der Dynamic Island, und der Zurück-Pfeil war tot.
.padding(.top, safeArea.top) .padding(.top, safeArea.top)
@@ -217,7 +217,7 @@ struct PlayerView: View {
// ein zweiter Klick auf dieselbe Stelle legte das Fenster // ein zweiter Klick auf dieselbe Stelle legte das Fenster
// ins Dock, weil das der Doppelklick auf eine // ins Dock, weil das der Doppelklick auf eine
// Fensterleiste tut. // Fensterleiste tut.
.padding(.top, FlixyPlatform.isMac ? Self.titleBarHeight : 0) .padding(.top, HaloPlatform.isMac ? Self.titleBarHeight : 0)
} }
} }
} }
@@ -334,12 +334,12 @@ struct PlayerView: View {
// kein Absturz, aber die falsche Farbe. // kein Absturz, aber die falsche Farbe.
.sheet(item: $trackPicker) { kind in .sheet(item: $trackPicker) { kind in
TrackPickerSheet(model: model, kind: kind) TrackPickerSheet(model: model, kind: kind)
.environment(\.flixyAccent, accent) .environment(\.haloAccent, accent)
} }
.sheet(isPresented: $showsQualityChoice) { .sheet(isPresented: $showsQualityChoice) {
QualitySheet(model: model) QualitySheet(model: model)
.environment(settings) .environment(settings)
.environment(\.flixyAccent, accent) .environment(\.haloAccent, accent)
} }
.onChange(of: showsQualityChoice) { _, isShown in .onChange(of: showsQualityChoice) { _, isShown in
// Nach dem Schließen die Bedienelemente stehen lassen und die // Nach dem Schließen die Bedienelemente stehen lassen und die
@@ -371,7 +371,7 @@ struct PlayerView: View {
Spacer() Spacer()
scrubber(for: model) scrubber(for: model)
} }
.padding(FlixySpacing.loose) .padding(HaloSpacing.loose)
// **Zusätzlich zum Polster, nicht statt.** Der Sicherheitsrand sagt, // **Zusätzlich zum Polster, nicht statt.** Der Sicherheitsrand sagt,
// wo der Bildschirm aufhört; das Polster sagt, wie luftig es aussehen // wo der Bildschirm aufhört; das Polster sagt, wie luftig es aussehen
// soll. Ersetzte eines das andere, klebte der Schieberegler im // soll. Ersetzte eines das andere, klebte der Schieberegler im
@@ -395,7 +395,7 @@ struct PlayerView: View {
} }
private func topBar(for model: PlayerModel) -> some View { private func topBar(for model: PlayerModel) -> some View {
HStack(alignment: .top, spacing: FlixySpacing.regular) { HStack(alignment: .top, spacing: HaloSpacing.regular) {
// Der Pfeil verkleinert, er beendet nicht. Das ist der ganze // Der Pfeil verkleinert, er beendet nicht. Das ist der ganze
// Zweck der Übung: weiterschauen, während man stöbert. // Zweck der Übung: weiterschauen, während man stöbert.
Button { Button {
@@ -403,7 +403,7 @@ struct PlayerView: View {
} label: { } label: {
Image(systemName: "chevron.down") Image(systemName: "chevron.down")
.font(.title3) .font(.title3)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.accessibilityLabel(Text("player.minimize")) .accessibilityLabel(Text("player.minimize"))
@@ -414,17 +414,17 @@ struct PlayerView: View {
} label: { } label: {
Image(systemName: "xmark") Image(systemName: "xmark")
.font(.callout.weight(.semibold)) .font(.callout.weight(.semibold))
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.accessibilityLabel(Text("player.stop")) .accessibilityLabel(Text("player.stop"))
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(model.title) Text(model.title)
.font(.flixyCardTitle) .font(.haloCardTitle)
.lineLimit(1) .lineLimit(1)
if let subtitle = model.subtitle { if let subtitle = model.subtitle {
Text(subtitle) Text(subtitle)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(.white.opacity(0.7)) .foregroundStyle(.white.opacity(0.7))
.lineLimit(1) .lineLimit(1)
} }
@@ -447,7 +447,7 @@ struct PlayerView: View {
? "arrow.down.right.and.arrow.up.left" ? "arrow.down.right.and.arrow.up.left"
: "arrow.up.left.and.arrow.down.right") : "arrow.up.left.and.arrow.down.right")
.font(.callout.weight(.semibold)) .font(.callout.weight(.semibold))
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.accessibilityLabel(Text(isFullScreen ? "player.exitFullScreen" : "player.fullScreen")) .accessibilityLabel(Text(isFullScreen ? "player.exitFullScreen" : "player.fullScreen"))
.keyboardShortcut("f", modifiers: [.command, .control]) .keyboardShortcut("f", modifiers: [.command, .control])
@@ -465,7 +465,7 @@ struct PlayerView: View {
? "arrow.down.right.and.arrow.up.left" ? "arrow.down.right.and.arrow.up.left"
: "arrow.up.left.and.arrow.down.right") : "arrow.up.left.and.arrow.down.right")
.font(.title3) .font(.title3)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.accessibilityLabel(Text(model.videoFit == .fill ? "player.fitVideo" : "player.fillVideo")) .accessibilityLabel(Text(model.videoFit == .fill ? "player.fitVideo" : "player.fillVideo"))
} }
@@ -488,7 +488,7 @@ struct PlayerView: View {
// Zwei Pfeile im Kreis sagen, was passiert: es dreht. // Zwei Pfeile im Kreis sagen, was passiert: es dreht.
Image(systemName: "arrow.triangle.2.circlepath") Image(systemName: "arrow.triangle.2.circlepath")
.font(.title3) .font(.title3)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.accessibilityLabel(Text("player.rotate")) .accessibilityLabel(Text("player.rotate"))
} }
@@ -524,7 +524,7 @@ struct PlayerView: View {
} label: { } label: {
Image(systemName: model.isPictureInPictureActive ? "pip.exit" : "pip.enter") Image(systemName: model.isPictureInPictureActive ? "pip.exit" : "pip.enter")
.font(.title3) .font(.title3)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.accessibilityLabel(Text("player.pictureInPicture")) .accessibilityLabel(Text("player.pictureInPicture"))
} }
@@ -532,7 +532,7 @@ struct PlayerView: View {
if model.engine?.supportsAirPlay == true { if model.engine?.supportsAirPlay == true {
AirPlayButton(tint: accent) AirPlayButton(tint: accent)
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
.accessibilityLabel(Text("player.airPlay")) .accessibilityLabel(Text("player.airPlay"))
} }
@@ -554,7 +554,7 @@ struct PlayerView: View {
} label: { } label: {
Image(systemName: symbol) Image(systemName: symbol)
.font(.title3) .font(.title3)
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
.accessibilityLabel(Text(label)) .accessibilityLabel(Text(label))
} }
@@ -578,15 +578,15 @@ struct PlayerView: View {
// sagen nicht, was eingestellt ist; „1080p" schon — und genau // sagen nicht, was eingestellt ist; „1080p" schon — und genau
// das ist die Frage, wegen der man hier hintippt. // das ist die Frage, wegen der man hier hintippt.
Text(model.qualityLabel) Text(model.qualityLabel)
.font(.flixyCaption.weight(.semibold)) .font(.haloCaption.weight(.semibold))
.monospacedDigit() .monospacedDigit()
.padding(.horizontal, FlixySpacing.snug) .padding(.horizontal, HaloSpacing.snug)
.padding(.vertical, FlixySpacing.tight) .padding(.vertical, HaloSpacing.tight)
.overlay { .overlay {
RoundedRectangle(cornerRadius: FlixyRadius.card) RoundedRectangle(cornerRadius: HaloRadius.card)
.strokeBorder(.white.opacity(0.5), lineWidth: 1) .strokeBorder(.white.opacity(0.5), lineWidth: 1)
} }
.padding(FlixySpacing.snug) .padding(HaloSpacing.snug)
} }
} }
.disabled(model.isChangingQuality) .disabled(model.isChangingQuality)
@@ -594,7 +594,7 @@ struct PlayerView: View {
} }
private func transport(for model: PlayerModel) -> some View { private func transport(for model: PlayerModel) -> some View {
HStack(spacing: FlixySpacing.section * 2) { HStack(spacing: HaloSpacing.section * 2) {
Button { Button {
model.skip(by: .seconds(-settings.rewindSeconds)) model.skip(by: .seconds(-settings.rewindSeconds))
scheduleControlsHide() scheduleControlsHide()
@@ -652,7 +652,7 @@ struct PlayerView: View {
} }
private func scrubber(for model: PlayerModel) -> some View { private func scrubber(for model: PlayerModel) -> some View {
VStack(spacing: FlixySpacing.tight) { VStack(spacing: HaloSpacing.tight) {
PlaybackScrubber( PlaybackScrubber(
position: model.currentTime, position: model.currentTime,
duration: model.duration ?? .seconds(1), duration: model.duration ?? .seconds(1),
@@ -695,9 +695,9 @@ struct PlayerView: View {
} }
} label: { } label: {
Label("player.nextEpisode", systemImage: "forward.end.fill") Label("player.nextEpisode", systemImage: "forward.end.fill")
.font(.flixyCardTitle) .font(.haloCardTitle)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
@@ -706,21 +706,21 @@ struct PlayerView: View {
model.skipActiveMarker() model.skipActiveMarker()
} label: { } label: {
Text(marker.isIntro ? "player.skipIntro" : "player.skipCredits") Text(marker.isIntro ? "player.skipIntro" : "player.skipCredits")
.font(.flixyCardTitle) .font(.haloCardTitle)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
.tint(.white) .tint(.white)
.transition(.move(edge: .trailing).combined(with: .opacity)) .transition(.move(edge: .trailing).combined(with: .opacity))
} }
} }
.padding(.horizontal, FlixySpacing.section) .padding(.horizontal, HaloSpacing.section)
.padding(.leading, safeArea.leading) .padding(.leading, safeArea.leading)
.padding(.trailing, safeArea.trailing) .padding(.trailing, safeArea.trailing)
// Über dem Schieberegler, damit sich beides nicht überdeckt, wenn // Über dem Schieberegler, damit sich beides nicht überdeckt, wenn
// die Bedienelemente eingeblendet sind. // die Bedienelemente eingeblendet sind.
.padding(.bottom, (showsControls ? 110 : FlixySpacing.section) + safeArea.bottom) .padding(.bottom, (showsControls ? 110 : HaloSpacing.section) + safeArea.bottom)
} }
.animation(.snappy, value: model.activeMarker?.id) .animation(.snappy, value: model.activeMarker?.id)
.animation(.snappy, value: model.hasReachedEnd) .animation(.snappy, value: model.hasReachedEnd)
@@ -731,10 +731,10 @@ struct PlayerView: View {
ZStack { ZStack {
Color.black.ignoresSafeArea() Color.black.ignoresSafeArea()
VStack(spacing: FlixySpacing.regular) { VStack(spacing: HaloSpacing.regular) {
ProgressView().tint(.white) ProgressView().tint(.white)
Text("player.restoring") Text("player.restoring")
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(.white) .foregroundStyle(.white)
// Die Stelle ausdrücklich nennen: der ganze Zweck ist, dass // Die Stelle ausdrücklich nennen: der ganze Zweck ist, dass
// niemand denkt, er müsse sie wiederfinden. // niemand denkt, er müsse sie wiederfinden.
@@ -747,33 +747,33 @@ struct PlayerView: View {
} }
private func preparing(for model: PlayerModel) -> some View { private func preparing(for model: PlayerModel) -> some View {
VStack(spacing: FlixySpacing.loose) { VStack(spacing: HaloSpacing.loose) {
ProgressView().tint(.white) ProgressView().tint(.white)
Text(model.title) Text(model.title)
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(.white) .foregroundStyle(.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text("player.preparing") Text("player.preparing")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(.white.opacity(0.6)) .foregroundStyle(.white.opacity(0.6))
} }
.padding(FlixySpacing.section) .padding(HaloSpacing.section)
} }
private func failure(message: String) -> some View { private func failure(message: String) -> some View {
VStack(spacing: FlixySpacing.loose) { VStack(spacing: HaloSpacing.loose) {
Image(systemName: "exclamationmark.triangle") Image(systemName: "exclamationmark.triangle")
.font(.largeTitle) .font(.largeTitle)
.foregroundStyle(.white.opacity(0.8)) .foregroundStyle(.white.opacity(0.8))
Text(message) Text(message)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(.white) .foregroundStyle(.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Button("action.close") { playback.close() } Button("action.close") { playback.close() }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
} }
.padding(FlixySpacing.section) .padding(HaloSpacing.section)
} }
/// Die beiden Flächen zum Springen. /// Die beiden Flächen zum Springen.
@@ -821,7 +821,7 @@ struct PlayerView: View {
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.system(size: 44)) .font(.system(size: 44))
.foregroundStyle(.white) .foregroundStyle(.white)
.padding(FlixySpacing.section) .padding(HaloSpacing.section)
.background(.black.opacity(0.35), in: .circle) .background(.black.opacity(0.35), in: .circle)
.transition(.opacity.combined(with: .scale)) .transition(.opacity.combined(with: .scale))
} }
@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyPlayback import HaloPlayback
/// Die Qualitätswahl im Player, samt dem, was gerade tatsächlich läuft. /// Die Qualitätswahl im Player, samt dem, was gerade tatsächlich läuft.
/// ///
@@ -11,7 +11,7 @@ import FlixyPlayback
struct QualitySheet: View { struct QualitySheet: View {
let model: PlayerModel let model: PlayerModel
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
/// **Als Fragezeichen**, nicht als Behauptung. /// **Als Fragezeichen**, nicht als Behauptung.
/// ///
/// `@Environment(PlaybackSettings.self)` ohne Optional hält die Anwendung /// `@Environment(PlaybackSettings.self)` ohne Optional hält die Anwendung
@@ -62,14 +62,14 @@ struct QualitySheet: View {
} }
} }
.navigationTitle(Text("player.quality")) .navigationTitle(Text("player.quality"))
.flixyInlineTitle() .haloInlineTitle()
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button("action.done") { dismiss() } Button("action.done") { dismiss() }
} }
} }
} }
.flixyResizableSheet() .haloResizableSheet()
} }
/// Was gerade läuft — gemessen, nicht gewünscht. /// Was gerade läuft — gemessen, nicht gewünscht.
@@ -158,11 +158,11 @@ struct QualitySheet: View {
} label: { } label: {
HStack { HStack {
Text(label(for: option)) Text(label(for: option))
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
if let detail { if let detail {
Text(detail) Text(detail)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
Spacer() Spacer()
if option == model.quality { if option == model.quality {
@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyPlayback import HaloPlayback
import MediaKit import MediaKit
/// Auswahl einer Spur — Ton **oder** Untertitel, nicht beides. /// Auswahl einer Spur — Ton **oder** Untertitel, nicht beides.
@@ -27,7 +27,7 @@ struct TrackPickerSheet: View {
let kind: Kind let kind: Kind
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@@ -68,7 +68,7 @@ struct TrackPickerSheet: View {
} label: { } label: {
Label("player.forgetRememberedTrack", systemImage: "arrow.uturn.backward") Label("player.forgetRememberedTrack", systemImage: "arrow.uturn.backward")
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} header: { } header: {
Text("player.rememberedTrack") Text("player.rememberedTrack")
} footer: { } footer: {
@@ -121,23 +121,23 @@ struct TrackPickerSheet: View {
} }
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text(kind.title)) .navigationTitle(Text(kind.title))
.flixyInlineTitle() .haloInlineTitle()
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button("action.close") { dismiss() } Button("action.close") { dismiss() }
} }
} }
} }
.flixyMediumSheet() .haloMediumSheet()
} }
private func row(title: String, isSelected: Bool, action: @escaping () -> Void) -> some View { private func row(title: String, isSelected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) { Button(action: action) {
HStack { HStack {
Text(title) Text(title)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Spacer() Spacer()
if isSelected { if isSelected {
Image(systemName: "checkmark").foregroundStyle(accent) Image(systemName: "checkmark").foregroundStyle(accent)
@@ -146,7 +146,7 @@ struct TrackPickerSheet: View {
.contentShape(.rect) .contentShape(.rect)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
.accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton) .accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton)
} }
} }
@@ -1,6 +1,6 @@
import AVKit import AVKit
import SwiftUI import SwiftUI
import FlixyPlayback import HaloPlayback
#if canImport(UIKit) #if canImport(UIKit)
import UIKit import UIKit
#else #else
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Sucht über alle Mediatheken des Servers. /// Sucht über alle Mediatheken des Servers.
@@ -8,7 +8,7 @@ import MediaKit
/// `SearchFeed`, weil der Fernseher dieselben Fragen an denselben Server /// `SearchFeed`, weil der Fernseher dieselben Fragen an denselben Server
/// stellt. Siehe dort, warum das kein Beiwerk ist. /// stellt. Siehe dort, warum das kein Beiwerk ist.
struct SearchView: View { struct SearchView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@State private var text = "" @State private var text = ""
@State private var feed = SearchFeed() @State private var feed = SearchFeed()
@@ -23,9 +23,9 @@ struct SearchView: View {
results results
} }
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("search.title")) .navigationTitle(Text("search.title"))
.flixyInlineTitle() .haloInlineTitle()
.searchable(text: $text) .searchable(text: $text)
// Jeder Tastendruck löst eine Anfrage aus — aber erst nach einer // Jeder Tastendruck löst eine Anfrage aus — aber erst nach einer
// kurzen Pause. Ohne sie schickt „Blade Runner" zwölf Anfragen los, // kurzen Pause. Ohne sie schickt „Blade Runner" zwölf Anfragen los,
@@ -35,12 +35,12 @@ struct SearchView: View {
// in der Suche nicht, und deshalb wirkte „als gesehen markieren" aus // in der Suche nicht, und deshalb wirkte „als gesehen markieren" aus
// dem Kontextmenü zwar am Server, aber die Kachel lud nie neu — es sah // dem Kontextmenü zwar am Server, aber die Kachel lud nie neu — es sah
// aus, als wäre nichts passiert. // aus, als wäre nichts passiert.
.environment(\.flixyItemChanged, { await feed.search(text, from: session.backend) }) .environment(\.haloItemChanged, { await feed.search(text, from: session.backend) })
} }
private var results: some View { private var results: some View {
ScrollView { ScrollView {
LazyVStack(alignment: .leading, spacing: FlixySpacing.section) { LazyVStack(alignment: .leading, spacing: HaloSpacing.section) {
ForEach(feed.hubs) { hub in ForEach(feed.hubs) { hub in
// **Antippen führt auf die Seite, nicht in den Film.** Die // **Antippen führt auf die Seite, nicht in den Film.** Die
// Suche benutzt dieselbe Reihe wie die Startseite — und // Suche benutzt dieselbe Reihe wie die Startseite — und
@@ -51,9 +51,9 @@ struct SearchView: View {
MediaRow(row: hub, opensDetails: true) MediaRow(row: hub, opensDetails: true)
} }
} }
.padding(.vertical, FlixySpacing.loose) .padding(.vertical, HaloSpacing.loose)
} }
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
@ViewBuilder @ViewBuilder
@@ -72,7 +72,7 @@ struct SearchView: View {
text = entry text = entry
} label: { } label: {
Label(entry, systemImage: "clock.arrow.circlepath") Label(entry, systemImage: "clock.arrow.circlepath")
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
} header: { } header: {
@@ -81,12 +81,12 @@ struct SearchView: View {
Button("search.forgetRecent", role: .destructive) { Button("search.forgetRecent", role: .destructive) {
feed.forgetRecent() feed.forgetRecent()
} }
.font(.flixyCaption) .font(.haloCaption)
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.flixyRevealsTopChrome() .haloRevealsTopChrome()
} }
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Die Farbeinstellungen. /// Die Farbeinstellungen.
/// ///
@@ -8,22 +8,22 @@ import FlixyDesign
/// Farbe greift die Kontrastkorrektur — und sagt es auch, statt die Farbe /// Farbe greift die Kontrastkorrektur — und sagt es auch, statt die Farbe
/// stillschweigend zu verändern. /// stillschweigend zu verändern.
struct AppearanceView: View { struct AppearanceView: View {
@Environment(FlixyTheme.self) private var theme @Environment(HaloTheme.self) private var theme
@Environment(HomeLayoutSettings.self) private var layout @Environment(HomeLayoutSettings.self) private var layout
@State private var customHex: String = "" @State private var customHex: String = ""
@State private var showsCustomField = false @State private var showsCustomField = false
private let columns = [GridItem(.adaptive(minimum: 64), spacing: FlixySpacing.regular)] private let columns = [GridItem(.adaptive(minimum: 64), spacing: HaloSpacing.regular)]
var body: some View { var body: some View {
Form { Form {
Section { Section {
LazyVGrid(columns: columns, spacing: FlixySpacing.regular) { LazyVGrid(columns: columns, spacing: HaloSpacing.regular) {
ForEach(AccentOption.palette) { option in ForEach(AccentOption.palette) { option in
swatch(option) swatch(option)
} }
} }
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
} header: { } header: {
Text("appearance.accent") Text("appearance.accent")
} footer: { } footer: {
@@ -38,7 +38,7 @@ struct AppearanceView: View {
LabeledContent { LabeledContent {
Text(layout.cardScale.formatted(.percent.precision(.fractionLength(0)))) Text(layout.cardScale.formatted(.percent.precision(.fractionLength(0))))
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.monospacedDigit() .monospacedDigit()
} label: { } label: {
Text("appearance.cardScale") Text("appearance.cardScale")
@@ -52,7 +52,7 @@ struct AppearanceView: View {
} footer: { } footer: {
Text("appearance.cardScaleExplanation") Text("appearance.cardScaleExplanation")
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
Section { Section {
Toggle("appearance.customColor", isOn: $showsCustomField.animation(.snappy)) Toggle("appearance.customColor", isOn: $showsCustomField.animation(.snappy))
@@ -60,7 +60,7 @@ struct AppearanceView: View {
if showsCustomField { if showsCustomField {
HStack { HStack {
TextField("appearance.hexPlaceholder", text: $customHex) TextField("appearance.hexPlaceholder", text: $customHex)
.flixyPlainEntry() .haloPlainEntry()
.onSubmit(applyCustomColor) .onSubmit(applyCustomColor)
Button("action.apply", action: applyCustomColor) Button("action.apply", action: applyCustomColor)
@@ -69,8 +69,8 @@ struct AppearanceView: View {
if !customHex.isEmpty, RGB(string: customHex) == nil { if !customHex.isEmpty, RGB(string: customHex) == nil {
Text("appearance.invalidHex") Text("appearance.invalidHex")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyDestructive) .foregroundStyle(Color.haloDestructive)
} }
} }
@@ -80,11 +80,11 @@ struct AppearanceView: View {
// als im Farbwähler, und niemand wüsste warum. // als im Farbwähler, und niemand wüsste warum.
Label { Label {
Text("appearance.adjusted \(theme.accent.hexString)") Text("appearance.adjusted \(theme.accent.hexString)")
.font(.flixyCaption) .font(.haloCaption)
} icon: { } icon: {
Image(systemName: "info.circle") Image(systemName: "info.circle")
} }
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
@@ -92,11 +92,11 @@ struct AppearanceView: View {
PreviewRow() PreviewRow()
} }
} }
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("appearance.title")) .navigationTitle(Text("appearance.title"))
.flixyInlineTitle() .haloInlineTitle()
.onAppear { .onAppear {
if theme.selection.id == AccentOption.customIdentifier { if theme.selection.id == AccentOption.customIdentifier {
customHex = theme.selection.color.hexString customHex = theme.selection.color.hexString
@@ -119,13 +119,13 @@ struct AppearanceView: View {
// Dunkel auf der Farbe, nicht weiß: die Palette // Dunkel auf der Farbe, nicht weiß: die Palette
// enthält helle Töne wie Amber, auf denen Weiß // enthält helle Töne wie Amber, auf denen Weiß
// verschwinden würde. // verschwinden würde.
.foregroundStyle(Color.flixyCanvas) .foregroundStyle(Color.haloCanvas)
} }
} }
.overlay { .overlay {
Circle() Circle()
.strokeBorder( .strokeBorder(
theme.selection == option ? Color.flixyTextPrimary : .clear, theme.selection == option ? Color.haloTextPrimary : .clear,
lineWidth: 2 lineWidth: 2
) )
.padding(-4) .padding(-4)
@@ -144,27 +144,27 @@ struct AppearanceView: View {
/// Zeigt die gewählte Farbe an den Stellen, an denen sie später auftaucht. /// Zeigt die gewählte Farbe an den Stellen, an denen sie später auftaucht.
private struct PreviewRow: View { private struct PreviewRow: View {
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
Label("hero.play", systemImage: "play.fill") Label("hero.play", systemImage: "play.fill")
.font(.flixyCardTitle) .font(.haloCardTitle)
.padding(.horizontal, FlixySpacing.loose) .padding(.horizontal, HaloSpacing.loose)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.background(accent, in: .rect(cornerRadius: FlixyRadius.control)) .background(accent, in: .rect(cornerRadius: HaloRadius.control))
.foregroundStyle(Color.flixyCanvas) .foregroundStyle(Color.haloCanvas)
Label("sidebar.downloads", systemImage: "arrow.down.circle") Label("sidebar.downloads", systemImage: "arrow.down.circle")
.font(.flixyCardTitle) .font(.haloCardTitle)
.foregroundStyle(accent) .foregroundStyle(accent)
} }
WatchProgressBar(fraction: 0.45) WatchProgressBar(fraction: 0.45)
} }
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// „Welche Tonsprache gilt hier?" — an genau zwei Orten dieselbe Frage. /// „Welche Tonsprache gilt hier?" — an genau zwei Orten dieselbe Frage.
/// ///
@@ -23,15 +23,15 @@ struct AudioLanguagePicker: View {
let title: LocalizedStringKey let title: LocalizedStringKey
@Environment(LanguagePreferences.self) private var languages @Environment(LanguagePreferences.self) private var languages
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
Text(title) Text(title)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Spacer(minLength: FlixySpacing.regular) Spacer(minLength: HaloSpacing.regular)
Menu { Menu {
// „Wie allgemein" zuerst und als Vorgabe: die meisten Ebenen // „Wie allgemein" zuerst und als Vorgabe: die meisten Ebenen
@@ -53,11 +53,11 @@ struct AudioLanguagePicker: View {
} }
} }
} label: { } label: {
HStack(spacing: FlixySpacing.tight) { HStack(spacing: HaloSpacing.tight) {
Text(currentLabel) Text(currentLabel)
.font(.flixyBody) .font(.haloBody)
Image(systemName: "chevron.up.chevron.down") Image(systemName: "chevron.up.chevron.down")
.font(.flixyCaption) .font(.haloCaption)
} }
.foregroundStyle(accent) .foregroundStyle(accent)
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Was beim letzten Abspielversuch passiert ist. /// Was beim letzten Abspielversuch passiert ist.
/// ///
@@ -7,8 +7,8 @@ import FlixyDesign
/// wurde, über welche Verbindung, und wie lange jeder Schritt gedauert hat. /// wurde, über welche Verbindung, und wie lange jeder Schritt gedauert hat.
/// Ein Screenshot davon sagt mehr als jede Beschreibung. /// Ein Screenshot davon sagt mehr als jede Beschreibung.
struct DiagnosticsView: View { struct DiagnosticsView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(PlaybackDiagnostics.self) private var diagnostics @Environment(PlaybackDiagnostics.self) private var diagnostics
var body: some View { var body: some View {
Form { Form {
@@ -17,14 +17,14 @@ struct DiagnosticsView: View {
LabeledContent("settings.serverName", value: session.serverName) LabeledContent("settings.serverName", value: session.serverName)
if let address = session.serverAddress { if let address = session.serverAddress {
LabeledContent("settings.address", value: address) LabeledContent("settings.address", value: address)
.font(.flixyCaption) .font(.haloCaption)
} }
if let beschreibung = session.connectionDescription { if let beschreibung = session.connectionDescription {
LabeledContent("settings.connection", value: beschreibung) LabeledContent("settings.connection", value: beschreibung)
.font(.flixyCaption) .font(.haloCaption)
} }
} else { } else {
Text("diagnostics.noServer").foregroundStyle(Color.flixyTextMuted) Text("diagnostics.noServer").foregroundStyle(Color.haloTextMuted)
} }
} }
@@ -36,11 +36,11 @@ struct DiagnosticsView: View {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("diagnostics.source") Text("diagnostics.source")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
Text(attempt.sourceDescription) Text(attempt.sourceDescription)
.font(.system(.caption2, design: .monospaced)) .font(.system(.caption2, design: .monospaced))
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
@@ -48,13 +48,13 @@ struct DiagnosticsView: View {
ForEach(Array(attempt.steps.enumerated()), id: \.offset) { _, step in ForEach(Array(attempt.steps.enumerated()), id: \.offset) { _, step in
Text(step) Text(step)
.font(.system(.caption, design: .monospaced)) .font(.system(.caption, design: .monospaced))
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
Section { Section {
Button { Button {
FlixyClipboard.copy(report(for: attempt)) HaloClipboard.copy(report(for: attempt))
} label: { } label: {
Label("diagnostics.copy", systemImage: "doc.on.doc") Label("diagnostics.copy", systemImage: "doc.on.doc")
} }
@@ -63,7 +63,7 @@ struct DiagnosticsView: View {
} }
} else { } else {
Section("diagnostics.lastAttempt") { Section("diagnostics.lastAttempt") {
Text("diagnostics.nothingYet").foregroundStyle(Color.flixyTextMuted) Text("diagnostics.nothingYet").foregroundStyle(Color.haloTextMuted)
} }
} }
@@ -79,11 +79,11 @@ struct DiagnosticsView: View {
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe // gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
// leuchteten, während die Schalter daneben schon die gewählte trugen. // leuchteten, während die Schalter daneben schon die gewählte trugen.
.tint(accent) .tint(accent)
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("diagnostics.title")) .navigationTitle(Text("diagnostics.title"))
.flixyInlineTitle() .haloInlineTitle()
} }
/// Der Bericht als Text zum Weitergeben. /// Der Bericht als Text zum Weitergeben.
@@ -92,7 +92,7 @@ struct DiagnosticsView: View {
/// Abfrageteil abgelegt, und der Servername verrät nichts Vertrauliches. /// Abfrageteil abgelegt, und der Servername verrät nichts Vertrauliches.
private func report(for attempt: PlaybackDiagnostics.Attempt) -> String { private func report(for attempt: PlaybackDiagnostics.Attempt) -> String {
""" """
Flixy \(Branding.version) · Build \(Branding.buildStamp) Halo \(Branding.version) · Build \(Branding.buildStamp)
Verbindung: \(attempt.connection) Verbindung: \(attempt.connection)
Titel: \(attempt.title) Titel: \(attempt.title)
Weg: \(attempt.method) Weg: \(attempt.method)
@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyDownloads import HaloDownloads
import MediaKit import MediaKit
/// Was beim Herunterladen gilt. /// Was beim Herunterladen gilt.
@@ -11,7 +11,7 @@ import MediaKit
/// rutschen. /// rutschen.
struct DownloadSettingsView: View { struct DownloadSettingsView: View {
@Environment(DownloadCoordinator.self) private var downloads @Environment(DownloadCoordinator.self) private var downloads
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
@Bindable var downloads = downloads @Bindable var downloads = downloads
@@ -73,11 +73,11 @@ struct DownloadSettingsView: View {
} }
} }
.tint(accent) .tint(accent)
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("sidebar.downloads")) .navigationTitle(Text("sidebar.downloads"))
.flixyInlineTitle() .haloInlineTitle()
} }
private var storageLimitBinding: Binding<Int64> { private var storageLimitBinding: Binding<Int64> {
@@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Reihen der Startseite ein- und ausblenden und umsortieren. /// Reihen der Startseite ein- und ausblenden und umsortieren.
struct HomeLayoutView: View { struct HomeLayoutView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@Environment(HomeLayoutSettings.self) private var layout @Environment(HomeLayoutSettings.self) private var layout
@State private var rows: [HomeRow] = [] @State private var rows: [HomeRow] = []
@@ -21,9 +21,9 @@ struct HomeLayoutView: View {
var body: some View { var body: some View {
list list
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("homeLayout.title")) .navigationTitle(Text("homeLayout.title"))
.flixyInlineTitle() .haloInlineTitle()
.task { await load() } .task { await load() }
} }
@@ -48,7 +48,7 @@ struct HomeLayoutView: View {
// gerade so weit hereinlugt, wie man es mag. // gerade so weit hereinlugt, wie man es mag.
LabeledContent { LabeledContent {
Text(layout.heroHeight.formatted(.percent.precision(.fractionLength(0)))) Text(layout.heroHeight.formatted(.percent.precision(.fractionLength(0))))
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.monospacedDigit() .monospacedDigit()
} label: { } label: {
Text("home.heroHeight") Text("home.heroHeight")
@@ -64,7 +64,7 @@ struct HomeLayoutView: View {
} footer: { } footer: {
Text("home.heroExplanation") Text("home.heroExplanation")
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
// **Mehrfachauswahl, und leer heisst alle.** Ein `Picker` könnte // **Mehrfachauswahl, und leer heisst alle.** Ein `Picker` könnte
// nur eine Mediathek, und „keine" wäre dort ein eigener Eintrag, // nur eine Mediathek, und „keine" wäre dort ein eigener Eintrag,
@@ -84,7 +84,7 @@ struct HomeLayoutView: View {
} }
} label: { } label: {
Text(library.title) Text(library.title)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
} }
// Damit der ganze Streifen trifft und nicht nur // Damit der ganze Streifen trifft und nicht nur
// die Schrift — eine Zeile, bei der nur ein Wort // die Schrift — eine Zeile, bei der nur ein Wort
@@ -99,7 +99,7 @@ struct HomeLayoutView: View {
} header: { } header: {
Text("home.heroLibraries") Text("home.heroLibraries")
} footer: { } footer: {
VStack(alignment: .leading, spacing: FlixySpacing.snug) { VStack(alignment: .leading, spacing: HaloSpacing.snug) {
Text("home.heroLibrariesExplanation") Text("home.heroLibrariesExplanation")
// Lieber eine Erklärung als ein Aufmacher, der // Lieber eine Erklärung als ein Aufmacher, der
// heimlich etwas anderes zeigt: trifft die Auswahl // heimlich etwas anderes zeigt: trifft die Auswahl
@@ -107,18 +107,18 @@ struct HomeLayoutView: View {
// dann muss dastehen, warum. // dann muss dastehen, warum.
if greiftInsLeere { if greiftInsLeere {
Text("home.heroLibrariesNoMatch") Text("home.heroLibrariesNoMatch")
.foregroundStyle(Color.flixyDestructive) .foregroundStyle(Color.haloDestructive)
} }
} }
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} }
Section { Section {
Toggle("home.combineContinue", isOn: $layout.combinesContinueRows) Toggle("home.combineContinue", isOn: $layout.combinesContinueRows)
Toggle("home.hideWatched", isOn: $layout.hidesWatchedInLatest) Toggle("home.hideWatched", isOn: $layout.hidesWatchedInLatest)
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
if isLoading { if isLoading {
ProgressView() ProgressView()
@@ -126,8 +126,8 @@ struct HomeLayoutView: View {
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
} else if rows.isEmpty { } else if rows.isEmpty {
Text("homeLayout.empty") Text("homeLayout.empty")
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} }
Section { Section {
@@ -140,9 +140,9 @@ struct HomeLayoutView: View {
} }
)) { )) {
Text(row.title) Text(row.title)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
} }
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} }
.onMove { indices, destination in .onMove { indices, destination in
rows.move(fromOffsets: indices, toOffset: destination) rows.move(fromOffsets: indices, toOffset: destination)
@@ -162,7 +162,7 @@ struct HomeLayoutView: View {
if !rows.isEmpty { if !rows.isEmpty {
Spacer() Spacer()
EditButton() EditButton()
.font(.flixyCaption) .font(.haloCaption)
.textCase(nil) .textCase(nil)
} }
#endif #endif
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Welche Sprachen der Player von sich aus wählen soll. /// Welche Sprachen der Player von sich aus wählen soll.
/// ///
@@ -9,7 +9,7 @@ import FlixyDesign
/// Vorliebe, die niemand äußern kann, ist keine. /// Vorliebe, die niemand äußern kann, ist keine.
struct LanguagesView: View { struct LanguagesView: View {
@Environment(LanguagePreferences.self) private var settings @Environment(LanguagePreferences.self) private var settings
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var isAdding = false @State private var isAdding = false
@@ -61,11 +61,11 @@ struct LanguagesView: View {
} }
} }
.tint(accent) .tint(accent)
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("languages.title")) .navigationTitle(Text("languages.title"))
.flixyInlineTitle() .haloInlineTitle()
// Eine Rangliste ohne Umsortieren wäre keine. Auf dem iPhone braucht // Eine Rangliste ohne Umsortieren wäre keine. Auf dem iPhone braucht
// das Ziehen den Bearbeitungsmodus nicht — `onMove` in einer `Form` // das Ziehen den Bearbeitungsmodus nicht — `onMove` in einer `Form`
// genügt, sobald die Liste bearbeitbar ist. // genügt, sobald die Liste bearbeitbar ist.
@@ -86,19 +86,19 @@ struct LanguagesView: View {
Section { Section {
if settings.fallbacks.isEmpty { if settings.fallbacks.isEmpty {
Text("languages.fallbackEmpty") Text("languages.fallbackEmpty")
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} else { } else {
ForEach(Array(settings.fallbacks.enumerated()), id: \.element) { position, code in ForEach(Array(settings.fallbacks.enumerated()), id: \.element) { position, code in
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
// Die Nummer, weil die Reihenfolge der ganze Zweck // Die Nummer, weil die Reihenfolge der ganze Zweck
// ist — eine Liste ohne sichtbaren Rang liest sich // ist — eine Liste ohne sichtbaren Rang liest sich
// wie eine Menge. // wie eine Menge.
Text("\(position + 1)") Text("\(position + 1)")
.font(.flixyCaption.monospacedDigit()) .font(.haloCaption.monospacedDigit())
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.frame(minWidth: 18, alignment: .trailing) .frame(minWidth: 18, alignment: .trailing)
Text(name(of: code)) Text(name(of: code))
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
} }
} }
.onMove { von, nach in .onMove { von, nach in
@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyPlayback import HaloPlayback
/// Alles, was die Wiedergabe betrifft. /// Alles, was die Wiedergabe betrifft.
/// ///
@@ -9,7 +9,7 @@ import FlixyPlayback
/// einmal durchgeht und dann in Ruhe lässt. /// einmal durchgeht und dann in Ruhe lässt.
struct PlaybackSettingsView: View { struct PlaybackSettingsView: View {
@Environment(PlaybackSettings.self) private var settings @Environment(PlaybackSettings.self) private var settings
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
@Bindable var settings = settings @Bindable var settings = settings
@@ -36,7 +36,7 @@ struct PlaybackSettingsView: View {
} header: { } header: {
Text("settings.quality") Text("settings.quality")
} footer: { } footer: {
VStack(alignment: .leading, spacing: FlixySpacing.snug) { VStack(alignment: .leading, spacing: HaloSpacing.snug) {
Text("settings.qualityExplanation") Text("settings.qualityExplanation")
// **Nur mit Beleg.** Eine Warnung, die immer dasteht, // **Nur mit Beleg.** Eine Warnung, die immer dasteht,
@@ -55,8 +55,8 @@ struct PlaybackSettingsView: View {
} icon: { } icon: {
Image(systemName: "exclamationmark.triangle") Image(systemName: "exclamationmark.triangle")
} }
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
} }
@@ -108,11 +108,11 @@ struct PlaybackSettingsView: View {
} }
} }
.tint(accent) .tint(accent)
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("settings.playback")) .navigationTitle(Text("settings.playback"))
.flixyInlineTitle() .haloInlineTitle()
} }
private func rateLabel(_ rate: Double) -> String { private func rateLabel(_ rate: Double) -> String {
@@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import MediaKit import MediaKit
/// Mit welchem Server gesprochen wird — und was er gerade anbietet. /// Mit welchem Server gesprochen wird — und was er gerade anbietet.
struct ServerSettingsView: View { struct ServerSettingsView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
Form { Form {
@@ -19,7 +19,7 @@ struct ServerSettingsView: View {
} }
if let address = session.serverAddress { if let address = session.serverAddress {
LabeledContent("settings.address", value: address) LabeledContent("settings.address", value: address)
.font(.flixyCaption) .font(.haloCaption)
.textSelection(.enabled) .textSelection(.enabled)
} }
if let accountName = session.accountName { if let accountName = session.accountName {
@@ -44,11 +44,11 @@ struct ServerSettingsView: View {
} }
} }
.tint(accent) .tint(accent)
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("settings.server")) .navigationTitle(Text("settings.server"))
.flixyInlineTitle() .haloInlineTitle()
} }
/// Zeigt, ob der Server im selben Netz steht — beim Suchen nach Ruckeln /// Zeigt, ob der Server im selben Netz steht — beim Suchen nach Ruckeln
@@ -64,20 +64,20 @@ struct ServerSettingsView: View {
struct LibraryRow: View { struct LibraryRow: View {
let library: MediaLibrary let library: MediaLibrary
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var phase: Phase = .idle @State private var phase: Phase = .idle
private enum Phase { case idle, running, done } private enum Phase { case idle, running, done }
var body: some View { var body: some View {
LabeledContent { LabeledContent {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
// Nur, wenn es nicht dasselbe Wort ist: eine Mediathek namens // Nur, wenn es nicht dasselbe Wort ist: eine Mediathek namens
// „Filme" stand sonst als „Filme — Filme" da. // „Filme" stand sonst als „Filme — Filme" da.
if library.title.localizedCaseInsensitiveCompare(library.kind.label) != .orderedSame { if library.title.localizedCaseInsensitiveCompare(library.kind.label) != .orderedSame {
Text(library.kind.label) Text(library.kind.label)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
if session.backend?.capabilities.contains(.libraryScan) == true { if session.backend?.capabilities.contains(.libraryScan) == true {
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Das farbige Symbol vor einer Einstellungszeile. /// Das farbige Symbol vor einer Einstellungszeile.
/// ///
@@ -35,7 +35,7 @@ struct SettingsHeader: View {
let explanation: LocalizedStringKey let explanation: LocalizedStringKey
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
Image(systemName: symbol) Image(systemName: symbol)
.font(.title2.weight(.semibold)) .font(.title2.weight(.semibold))
.foregroundStyle(.white) .foregroundStyle(.white)
@@ -43,16 +43,16 @@ struct SettingsHeader: View {
.background(colour.gradient, in: .rect(cornerRadius: 13)) .background(colour.gradient, in: .rect(cornerRadius: 13))
Text(title) Text(title)
.font(.flixyTitle) .font(.haloTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Text(explanation) Text(explanation)
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, FlixySpacing.snug) .padding(.vertical, HaloSpacing.snug)
.listRowBackground(Color.flixySurface) .listRowBackground(Color.haloSurface)
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Die Einstellungen — als Übersicht, nicht als eine lange Liste. /// Die Einstellungen — als Übersicht, nicht als eine lange Liste.
/// ///
@@ -10,8 +10,8 @@ import FlixyDesign
/// ///
/// Jetzt führt jede Zeile in einen Bildschirm, der zu **einer** Frage gehört. /// Jetzt führt jede Zeile in einen Bildschirm, der zu **einer** Frage gehört.
struct SettingsView: View { struct SettingsView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var showsSignOutConfirmation = false @State private var showsSignOutConfirmation = false
var body: some View { var body: some View {
@@ -23,7 +23,7 @@ struct SettingsView: View {
} label: { } label: {
LabeledContent { LabeledContent {
Text(session.serverName) Text(session.serverName)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} label: { } label: {
row( row(
"settings.server", "settings.server",
@@ -83,12 +83,12 @@ struct SettingsView: View {
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe // gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
// leuchteten, während die Schalter daneben schon die gewählte trugen. // leuchteten, während die Schalter daneben schon die gewählte trugen.
.tint(accent) .tint(accent)
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.flixyRevealsTopChrome() .haloRevealsTopChrome()
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("sidebar.settings")) .navigationTitle(Text("sidebar.settings"))
.flixyInlineTitle() .haloInlineTitle()
.confirmationDialog( .confirmationDialog(
Text("settings.signOutConfirm"), Text("settings.signOutConfirm"),
isPresented: $showsSignOutConfirmation, isPresented: $showsSignOutConfirmation,
@@ -115,7 +115,7 @@ struct SettingsView: View {
} }
private func row(_ title: LocalizedStringKey, symbol: String, colour: Color) -> some View { private func row(_ title: LocalizedStringKey, symbol: String, colour: Color) -> some View {
HStack(spacing: FlixySpacing.regular) { HStack(spacing: HaloSpacing.regular) {
SettingsIcon(symbol: symbol, colour: colour) SettingsIcon(symbol: symbol, colour: colour)
Text(title) Text(title)
} }
@@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyPlayback import HaloPlayback
/// Wie Untertitel aussehen sollen. /// Wie Untertitel aussehen sollen.
struct SubtitleAppearanceView: View { struct SubtitleAppearanceView: View {
@Environment(SubtitleAppearance.self) private var appearance @Environment(SubtitleAppearance.self) private var appearance
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
var body: some View { var body: some View {
@Bindable var appearance = appearance @Bindable var appearance = appearance
@@ -51,11 +51,11 @@ struct SubtitleAppearanceView: View {
} }
} }
.tint(accent) .tint(accent)
.flixyForm() .haloForm()
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("subtitleStyle.title")) .navigationTitle(Text("subtitleStyle.title"))
.flixyInlineTitle() .haloInlineTitle()
} }
/// Ein Beispielsatz über grauer Fläche. /// Ein Beispielsatz über grauer Fläche.
@@ -84,10 +84,10 @@ struct SubtitleAppearanceView: View {
radius: 2, radius: 2,
y: 1 y: 1
) )
.padding(FlixySpacing.regular) .padding(HaloSpacing.regular)
} }
.frame(height: 96) .frame(height: 96)
.clipShape(.rect(cornerRadius: FlixyRadius.card)) .clipShape(.rect(cornerRadius: HaloRadius.card))
.listRowInsets(EdgeInsets()) .listRowInsets(EdgeInsets())
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
} }
@@ -1,21 +1,21 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Während ein Weg zum Server gesucht wird. /// Während ein Weg zum Server gesucht wird.
struct ConnectingView: View { struct ConnectingView: View {
let serverName: String let serverName: String
var body: some View { var body: some View {
VStack(spacing: FlixySpacing.loose) { VStack(spacing: HaloSpacing.loose) {
ProgressView() ProgressView()
Text(serverName.isEmpty Text(serverName.isEmpty
? String(localized: "connecting.searching") ? String(localized: "connecting.searching")
: String(localized: "connecting.to \(serverName)")) : String(localized: "connecting.to \(serverName)"))
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
} }
} }
@@ -33,8 +33,8 @@ struct ConnectionFailureView: View {
} actions: { } actions: {
Button("action.retry", action: onRetry).buttonStyle(.borderedProminent) Button("action.retry", action: onRetry).buttonStyle(.borderedProminent)
Button("action.signOut", action: onSignOut) Button("action.signOut", action: onSignOut)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
.background(Color.flixyCanvas) .background(Color.haloCanvas)
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import JellyfinKit import JellyfinKit
/// Die Anmeldung an einem Jellyfin-Server. /// Die Anmeldung an einem Jellyfin-Server.
@@ -9,9 +9,9 @@ import JellyfinKit
/// diesen Code. Der Weg über den Code steht vorn, weil dabei kein Passwort /// diesen Code. Der Weg über den Code steht vorn, weil dabei kein Passwort
/// durch diese App läuft. /// durch diese App läuft.
struct JellyfinSignInView: View { struct JellyfinSignInView: View {
@Environment(FlixySession.self) private var session @Environment(HaloSession.self) private var session
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var model: JellyfinSignInModel? @State private var model: JellyfinSignInModel?
@FocusState private var focusedField: Field? @FocusState private var focusedField: Field?
@@ -23,16 +23,16 @@ struct JellyfinSignInView: View {
ScrollView { ScrollView {
if let model { if let model {
content(for: model) content(for: model)
.padding(.horizontal, FlixySpacing.section) .padding(.horizontal, HaloSpacing.section)
.padding(.vertical, FlixySpacing.section) .padding(.vertical, HaloSpacing.section)
.frame(maxWidth: 420) .frame(maxWidth: 420)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboard(.interactively)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.navigationTitle(Text("signIn.jellyfin.title")) .navigationTitle(Text("signIn.jellyfin.title"))
.flixyInlineTitle() .haloInlineTitle()
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("action.cancel") { Button("action.cancel") {
@@ -66,7 +66,7 @@ struct JellyfinSignInView: View {
private func content(for model: JellyfinSignInModel) -> some View { private func content(for model: JellyfinSignInModel) -> some View {
@Bindable var model = model @Bindable var model = model
VStack(alignment: .leading, spacing: FlixySpacing.section) { VStack(alignment: .leading, spacing: HaloSpacing.section) {
switch model.step { switch model.step {
case .address: case .address:
addressStep(model: model) addressStep(model: model)
@@ -82,8 +82,8 @@ struct JellyfinSignInView: View {
if let message = model.errorMessage { if let message = model.errorMessage {
Label(message, systemImage: "exclamationmark.triangle") Label(message, systemImage: "exclamationmark.triangle")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyDestructive) .foregroundStyle(Color.haloDestructive)
} }
} }
} }
@@ -94,39 +94,39 @@ struct JellyfinSignInView: View {
private func addressStep(model: JellyfinSignInModel) -> some View { private func addressStep(model: JellyfinSignInModel) -> some View {
@Bindable var model = model @Bindable var model = model
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
Text("signIn.jellyfin.addressPrompt") Text("signIn.jellyfin.addressPrompt")
.font(.flixyRowHeader) .font(.haloRowHeader)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
TextField("signIn.jellyfin.addressPlaceholder", text: $model.address) TextField("signIn.jellyfin.addressPlaceholder", text: $model.address)
.textFieldStyle(.plain) .textFieldStyle(.plain)
.textContentType(.URL) .textContentType(.URL)
.flixyPlainEntry(isAddress: true) .haloPlainEntry(isAddress: true)
.submitLabel(.go) .submitLabel(.go)
.focused($focusedField, equals: .address) .focused($focusedField, equals: .address)
.onSubmit { Task { await model.findServer() } } .onSubmit { Task { await model.findServer() } }
// Hier statt beim Öffnen des Sheets: so bekommt das Feld den // Hier statt beim Öffnen des Sheets: so bekommt das Feld den
// Fokus auch dann, wenn man nach einem Vertipper zurückkommt. // Fokus auch dann, wenn man nach einem Vertipper zurückkommt.
.task { focusedField = .address } .task { focusedField = .address }
.padding(FlixySpacing.regular) .padding(HaloSpacing.regular)
.background(Color.flixyElevated, in: .rect(cornerRadius: FlixyRadius.control)) .background(Color.haloElevated, in: .rect(cornerRadius: HaloRadius.control))
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
// Ein Beispiel statt einer Erklärung: „Schema, Host, Port" sagt // Ein Beispiel statt einer Erklärung: „Schema, Host, Port" sagt
// niemandem etwas, „192.168.1.50" jedem. // niemandem etwas, „192.168.1.50" jedem.
Text("signIn.jellyfin.addressHint") Text("signIn.jellyfin.addressHint")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
Button { Button {
focusedField = nil focusedField = nil
Task { await model.findServer() } Task { await model.findServer() }
} label: { } label: {
Text("signIn.jellyfin.connect") Text("signIn.jellyfin.connect")
.font(.flixyRowHeader) .font(.haloRowHeader)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
@@ -138,40 +138,40 @@ struct JellyfinSignInView: View {
@ViewBuilder @ViewBuilder
private func quickConnectStep(model: JellyfinSignInModel, code: String) -> some View { private func quickConnectStep(model: JellyfinSignInModel, code: String) -> some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
foundServerHeader(model: model) foundServerHeader(model: model)
Text("signIn.jellyfin.quickConnectHint") Text("signIn.jellyfin.quickConnectHint")
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
Text(code) Text(code)
.font(.system(.largeTitle, design: .monospaced, weight: .semibold)) .font(.system(.largeTitle, design: .monospaced, weight: .semibold))
.tracking(8) .tracking(8)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
.textSelection(.enabled) .textSelection(.enabled)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(Color.flixyElevated, in: .rect(cornerRadius: FlixyRadius.control)) .background(Color.haloElevated, in: .rect(cornerRadius: HaloRadius.control))
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
ProgressView().controlSize(.small) ProgressView().controlSize(.small)
Text("signIn.jellyfin.waitingForApproval") Text("signIn.jellyfin.waitingForApproval")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
} }
Text("signIn.jellyfin.privacy") Text("signIn.jellyfin.privacy")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
Divider().overlay(Color.flixySeparator) Divider().overlay(Color.haloSeparator)
Button("signIn.jellyfin.usePassword") { model.useCredentials() } Button("signIn.jellyfin.usePassword") { model.useCredentials() }
.font(.flixyCaption) .font(.haloCaption)
Button("signIn.jellyfin.otherAddress") { model.startOver() } Button("signIn.jellyfin.otherAddress") { model.startOver() }
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
} }
@@ -181,12 +181,12 @@ struct JellyfinSignInView: View {
private func credentialsStep(model: JellyfinSignInModel) -> some View { private func credentialsStep(model: JellyfinSignInModel) -> some View {
@Bindable var model = model @Bindable var model = model
VStack(alignment: .leading, spacing: FlixySpacing.regular) { VStack(alignment: .leading, spacing: HaloSpacing.regular) {
foundServerHeader(model: model) foundServerHeader(model: model)
TextField("signIn.jellyfin.userName", text: $model.userName) TextField("signIn.jellyfin.userName", text: $model.userName)
.textContentType(.username) .textContentType(.username)
.flixyPlainEntry() .haloPlainEntry()
.submitLabel(.next) .submitLabel(.next)
.focused($focusedField, equals: .userName) .focused($focusedField, equals: .userName)
.onSubmit { focusedField = .password } .onSubmit { focusedField = .password }
@@ -200,8 +200,8 @@ struct JellyfinSignInView: View {
focusedField = .password focusedField = .password
return .handled return .handled
} }
.padding(FlixySpacing.regular) .padding(HaloSpacing.regular)
.background(Color.flixyElevated, in: .rect(cornerRadius: FlixyRadius.control)) .background(Color.haloElevated, in: .rect(cornerRadius: HaloRadius.control))
SecureField("signIn.jellyfin.password", text: $model.password) SecureField("signIn.jellyfin.password", text: $model.password)
.textContentType(.password) .textContentType(.password)
@@ -214,17 +214,17 @@ struct JellyfinSignInView: View {
focusedField = .userName focusedField = .userName
return .handled return .handled
} }
.padding(FlixySpacing.regular) .padding(HaloSpacing.regular)
.background(Color.flixyElevated, in: .rect(cornerRadius: FlixyRadius.control)) .background(Color.haloElevated, in: .rect(cornerRadius: HaloRadius.control))
Button { Button {
focusedField = nil focusedField = nil
Task { await model.signInWithPassword() } Task { await model.signInWithPassword() }
} label: { } label: {
Text("signIn.jellyfin.submit") Text("signIn.jellyfin.submit")
.font(.flixyRowHeader) .font(.haloRowHeader)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
@@ -234,46 +234,46 @@ struct JellyfinSignInView: View {
// verworfen. Gespeichert wird nur der Token, und der im // verworfen. Gespeichert wird nur der Token, und der im
// Schlüsselbund. // Schlüsselbund.
Text("signIn.jellyfin.passwordNote") Text("signIn.jellyfin.passwordNote")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
Divider().overlay(Color.flixySeparator) Divider().overlay(Color.haloSeparator)
if model.offersQuickConnect { if model.offersQuickConnect {
Button("signIn.jellyfin.useCode") { Task { await model.beginQuickConnect() } } Button("signIn.jellyfin.useCode") { Task { await model.beginQuickConnect() } }
.font(.flixyCaption) .font(.haloCaption)
} }
Button("signIn.jellyfin.otherAddress") { model.startOver() } Button("signIn.jellyfin.otherAddress") { model.startOver() }
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
} }
// MARK: - Bausteine // MARK: - Bausteine
@ViewBuilder @ViewBuilder
private func foundServerHeader(model: JellyfinSignInModel) -> some View { private func foundServerHeader(model: JellyfinSignInModel) -> some View {
VStack(alignment: .leading, spacing: FlixySpacing.tight) { VStack(alignment: .leading, spacing: HaloSpacing.tight) {
Text("signIn.jellyfin.foundServer") Text("signIn.jellyfin.foundServer")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.textCase(.uppercase) .textCase(.uppercase)
Text(model.serverName ?? model.address) Text(model.serverName ?? model.address)
.font(.flixyTitle) .font(.haloTitle)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
} }
} }
@ViewBuilder @ViewBuilder
private func waiting(text: LocalizedStringKey) -> some View { private func waiting(text: LocalizedStringKey) -> some View {
HStack(spacing: FlixySpacing.snug) { HStack(spacing: HaloSpacing.snug) {
ProgressView().controlSize(.small) ProgressView().controlSize(.small)
Text(text) Text(text)
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
} }
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, FlixySpacing.section) .padding(.vertical, HaloSpacing.section)
} }
} }
@@ -1,5 +1,5 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
/// Der Anmeldebildschirm. /// Der Anmeldebildschirm.
/// ///
@@ -12,54 +12,54 @@ import FlixyDesign
/// wird die Adresse eingetippt und, wann immer der Server es anbietet, ein /// wird die Adresse eingetippt und, wann immer der Server es anbietet, ein
/// Code bestätigt statt eines Passworts — das führt `JellyfinSignInView`. /// Code bestätigt statt eines Passworts — das führt `JellyfinSignInView`.
struct SignInView: View { struct SignInView: View {
@Environment(\.flixyAccent) private var accent @Environment(\.haloAccent) private var accent
@State private var showsJellyfinSignIn = false @State private var showsJellyfinSignIn = false
var body: some View { var body: some View {
VStack(spacing: FlixySpacing.section) { VStack(spacing: HaloSpacing.section) {
Spacer() Spacer()
VStack(spacing: FlixySpacing.regular) { VStack(spacing: HaloSpacing.regular) {
Image(systemName: "play.rectangle.on.rectangle") Image(systemName: "play.rectangle.on.rectangle")
.font(.system(size: 52)) .font(.system(size: 52))
.foregroundStyle(accent) .foregroundStyle(accent)
Text(Branding.appName) Text(Branding.appName)
.font(.flixyHero) .font(.haloHero)
.foregroundStyle(Color.flixyTextPrimary) .foregroundStyle(Color.haloTextPrimary)
Text("signIn.tagline") Text("signIn.tagline")
.font(.flixyBody) .font(.haloBody)
.foregroundStyle(Color.flixyTextSecondary) .foregroundStyle(Color.haloTextSecondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
Spacer() Spacer()
VStack(spacing: FlixySpacing.regular) { VStack(spacing: HaloSpacing.regular) {
Button { Button {
showsJellyfinSignIn = true showsJellyfinSignIn = true
} label: { } label: {
Text("signIn.withJellyfin") Text("signIn.withJellyfin")
.font(.flixyRowHeader) .font(.haloRowHeader)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, FlixySpacing.regular) .padding(.vertical, HaloSpacing.regular)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(accent) .tint(accent)
Text("signIn.privacy") Text("signIn.privacy")
.font(.flixyCaption) .font(.haloCaption)
.foregroundStyle(Color.flixyTextMuted) .foregroundStyle(Color.haloTextMuted)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
Spacer().frame(height: FlixySpacing.section) Spacer().frame(height: HaloSpacing.section)
} }
.padding(.horizontal, FlixySpacing.section) .padding(.horizontal, HaloSpacing.section)
.frame(maxWidth: 420) .frame(maxWidth: 420)
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.flixyCanvas) .background(Color.haloCanvas)
.sheet(isPresented: $showsJellyfinSignIn) { .sheet(isPresented: $showsJellyfinSignIn) {
JellyfinSignInView() JellyfinSignInView()
} }
@@ -4,7 +4,7 @@
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.scarriffleservices.flixy</string> <string>group.com.scarriffleservices.halo</string>
</array> </array>
</dict> </dict>
</plist> </plist>
@@ -1,12 +1,12 @@
import SwiftUI import SwiftUI
import FlixyDesign import HaloDesign
import FlixyPlayback import HaloPlayback
import MediaKit import MediaKit
import JellyfinKit import JellyfinKit
/// Die App auf dem Fernseher. /// Die App auf dem Fernseher.
/// ///
/// **Eine eigene Wurzel statt eines Zweigs in `FlixyApp`.** Der Unterschied /// **Eine eigene Wurzel statt eines Zweigs in `HaloApp`.** Der Unterschied
/// zwischen Telefon und Fernseher ist keine Feinheit der Darstellung, sondern /// zwischen Telefon und Fernseher ist keine Feinheit der Darstellung, sondern
/// die Bedienart: kein Finger, keine Grössenklasse, keine Werkzeugleiste, kein /// die Bedienart: kein Finger, keine Grössenklasse, keine Werkzeugleiste, kein
/// Ziehen zum Aktualisieren, keine halbhohen Sheets — und dafür ein Fokus, den /// Ziehen zum Aktualisieren, keine halbhohen Sheets — und dafür ein Fokus, den
@@ -14,7 +14,7 @@ import JellyfinKit
/// wäre kein Zweig, sondern ein zweiter Rumpf in derselben Datei, und der /// wäre kein Zweig, sondern ein zweiter Rumpf in derselben Datei, und der
/// nächste, der die iOS-Seite ändert, bräche die andere, ohne es zu merken. /// nächste, der die iOS-Seite ändert, bräche die andere, ohne es zu merken.
/// ///
/// **Die Modelle dagegen werden geteilt**, nicht kopiert: `Flixy/Session/` /// **Die Modelle dagegen werden geteilt**, nicht kopiert: `Halo/Session/`
/// kommt über einen Pfadeintrag in `project.yml` mit. Die Naht verläuft /// kommt über einen Pfadeintrag in `project.yml` mit. Die Naht verläuft
/// zwischen Modell und Ansicht, nicht zwischen den Plattformen. /// zwischen Modell und Ansicht, nicht zwischen den Plattformen.
/// ///
@@ -22,9 +22,9 @@ import JellyfinKit
/// iOS-App trägt ausschliesslich Downloads, und die gibt es hier nicht — damit /// iOS-App trägt ausschliesslich Downloads, und die gibt es hier nicht — damit
/// fällt beides weg. /// fällt beides weg.
@main @main
struct FlixyTVApp: App { struct HaloTVApp: App {
@State private var session: FlixySession @State private var session: HaloSession
@State private var theme = FlixyTheme() @State private var theme = HaloTheme()
@State private var playback = PlaybackCoordinator() @State private var playback = PlaybackCoordinator()
@State private var playbackSettings = PlaybackSettings() @State private var playbackSettings = PlaybackSettings()
@State private var languages = LanguagePreferences() @State private var languages = LanguagePreferences()
@@ -40,7 +40,7 @@ struct FlixyTVApp: App {
@State private var network = NetworkConditions() @State private var network = NetworkConditions()
init() { init() {
_session = State(initialValue: FlixySession( _session = State(initialValue: HaloSession(
jellyfinIdentity: JellyfinIdentity.current( jellyfinIdentity: JellyfinIdentity.current(
client: Branding.appName, client: Branding.appName,
version: Branding.version version: Branding.version
@@ -62,7 +62,7 @@ struct FlixyTVApp: App {
.environment(homeLayout) .environment(homeLayout)
.environment(diagnostics) .environment(diagnostics)
.environment(network) .environment(network)
.flixyAccent(theme) .haloAccent(theme)
.tint(theme.accentColor) .tint(theme.accentColor)
// Ein Fernseher ist immer dunkel. Der Schalter in der // Ein Fernseher ist immer dunkel. Der Schalter in der
// Info.plist gilt fürs System, diese Zeile für die Vorschauen. // Info.plist gilt fürs System, diese Zeile für die Vorschauen.
+4 -4
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Flixy</string> <string>Halo</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -22,10 +22,10 @@
<array> <array>
<dict> <dict>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string>com.scarriffleservices.flixy.topshelf</string> <string>com.scarriffleservices.halo.topshelf</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>flixy</string> <string>halo</string>
</array> </array>
</dict> </dict>
</array> </array>
@@ -39,7 +39,7 @@
<true/> <true/>
</dict> </dict>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Flixy benötigt Zugriff auf das lokale Netzwerk, um deinen Jellyfin-Server zu erreichen.</string> <string>Halo benötigt Zugriff auf das lokale Netzwerk, um deinen Jellyfin-Server zu erreichen.</string>
<key>UIUserInterfaceStyle</key> <key>UIUserInterfaceStyle</key>
<string>Dark</string> <string>Dark</string>
</dict> </dict>

Some files were not shown because too many files have changed in this diff Show More