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:
co-authored by
Claude Opus 5
parent
2fcf6821c8
commit
4b9a1e7b67
+3
-3
@@ -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/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
-1
@@ -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 |
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
+1
-1
@@ -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.
|
||||
///
|
||||
+2
-2
@@ -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.
|
||||
///
|
||||
+1
-1
@@ -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 = {}
|
||||
}
|
||||
+9
-9
@@ -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)
|
||||
}
|
||||
+40
-40
@@ -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)
|
||||
}
|
||||
+41
-41
@@ -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)
|
||||
}
|
||||
+18
-18
@@ -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)
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -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
-1
@@ -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.
|
||||
+40
-40
@@ -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()
|
||||
+13
-13
@@ -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())
|
||||
}
|
||||
+7
-7
@@ -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)
|
||||
}
|
||||
+12
-12
@@ -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 {
|
||||
+6
-6
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+69
-69
@@ -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())
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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.
|
||||
+2
-2
@@ -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 {
|
||||
+9
-9
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-28
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -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)
|
||||
}
|
||||
+16
-16
@@ -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)
|
||||
+6
-6
@@ -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> {
|
||||
+17
-17
@@ -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
|
||||
+10
-10
@@ -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
|
||||
+9
-9
@@ -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 {
|
||||
+11
-11
@@ -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 {
|
||||
+8
-8
@@ -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)
|
||||
}
|
||||
+8
-8
@@ -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)
|
||||
}
|
||||
+7
-7
@@ -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)
|
||||
}
|
||||
}
|
||||
+60
-60
@@ -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.
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user