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

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