Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f98b995050
commit
a7cf6c5def
+3
-3
@@ -9,10 +9,10 @@ xcuserdata/
|
||||
# Von XcodeGen aus project.yml erzeugt — nicht eingecheckt, damit es keine
|
||||
# Konflikte in der pbxproj gibt. Nach dem Klonen: xcodegen generate
|
||||
#
|
||||
# Halo.xcworkspace daneben ist dagegen von Hand geschrieben und gehört dazu:
|
||||
# Flaro.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 Halo.xcworkspace/README.md.
|
||||
Halo.xcodeproj/
|
||||
# nötig ist, steht in Flaro.xcworkspace/README.md.
|
||||
Flaro.xcodeproj/
|
||||
node_modules/
|
||||
|
||||
# .NET
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import ActivityKit
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
import HaloDesign
|
||||
import HaloDownloads
|
||||
import FlaroDesign
|
||||
import FlaroDownloads
|
||||
|
||||
/// 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>Halo Downloads</string>
|
||||
<string>Flaro Downloads</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Diesen Arbeitsbereich öffnen, nicht das Projekt
|
||||
|
||||
`Halo.xcodeproj` wird von XcodeGen aus `project.yml` erzeugt — und dabei
|
||||
`Flaro.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 (`Halo.xcodeproj/project.xcworkspace`), meldet es danach
|
||||
Projekt offen (`Flaro.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:Halo.xcodeproj"></FileRef>
|
||||
<FileRef location = "group:Flaro.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 „Halo" zu, sondern immer hierauf. Zu ändern sind dann genau
|
||||
/// geschriebenes „Flaro" zu, sondern immer hierauf. Zu ändern sind dann genau
|
||||
/// zwei Dinge: diese Datei und `project.yml` (Produktname, Bundle-Kennung,
|
||||
/// Anzeigename).
|
||||
///
|
||||
/// Die Typen aus HaloKit behalten ihre Namen — sie sind Bauteile, keine Marke.
|
||||
/// Die Typen aus FlaroKit behalten ihre Namen — sie sind Bauteile, keine Marke.
|
||||
enum Branding {
|
||||
/// Wie die App sich selbst nennt.
|
||||
static let appName = "Halo"
|
||||
static let appName = "Flaro"
|
||||
|
||||
/// 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.halo"
|
||||
Bundle.main.bundleIdentifier ?? "com.scarriffleservices.flaro"
|
||||
}
|
||||
|
||||
/// Wann dieser Build entstanden ist.
|
||||
@@ -3,14 +3,14 @@ import SwiftUI
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
import HaloDesign
|
||||
import HaloDownloads
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroDownloads
|
||||
import FlaroPlayback
|
||||
import JellyfinKit
|
||||
import MediaKit
|
||||
|
||||
@main
|
||||
struct HaloApp: App {
|
||||
struct FlaroApp: 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 HaloApp: 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 = HaloTheme()
|
||||
@State private var session: HaloSession
|
||||
@State private var theme = FlaroTheme()
|
||||
@State private var session: FlaroSession
|
||||
@State private var playback = PlaybackCoordinator()
|
||||
@State private var playbackSettings = PlaybackSettings()
|
||||
@State private var languages = LanguagePreferences()
|
||||
@@ -40,7 +40,7 @@ struct HaloApp: App {
|
||||
private let modelContainer: ModelContainer
|
||||
|
||||
init() {
|
||||
_session = State(initialValue: HaloSession(
|
||||
_session = State(initialValue: FlaroSession(
|
||||
jellyfinIdentity: JellyfinIdentity.current(
|
||||
client: Branding.appName,
|
||||
version: Branding.version
|
||||
@@ -74,8 +74,8 @@ struct HaloApp: App {
|
||||
// Notizzettelgröße, und darin fällt der Aufmacher in sich
|
||||
// zusammen, weil seine Abstände dehnbar sind.
|
||||
.frame(
|
||||
minWidth: HaloPlatform.isMac ? 900 : nil,
|
||||
minHeight: HaloPlatform.isMac ? 620 : nil
|
||||
minWidth: FlaroPlatform.isMac ? 900 : nil,
|
||||
minHeight: FlaroPlatform.isMac ? 620 : nil
|
||||
)
|
||||
// `defaultSize` gibt es hier nicht; die Größe hängt an der
|
||||
// Szene.
|
||||
@@ -133,7 +133,7 @@ struct HaloApp: App {
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
extension HaloApp {
|
||||
extension FlaroApp {
|
||||
/// 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>Halo</string>
|
||||
<string>Flaro</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -28,7 +28,7 @@
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Halo benötigt Zugriff auf das lokale Netzwerk, um deinen Jellyfin-Server zu erreichen und Medien direkt von ihm zu laden.</string>
|
||||
<string>Flaro 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 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."
|
||||
"value": "Die Obergrenze, nicht das Ziel. Im eigenen Netz überträgt der Server das Original; unterwegs beginnt Flaro 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 Halo 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 Flaro 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 HaloDownloads
|
||||
import FlaroDownloads
|
||||
import MediaKit
|
||||
|
||||
/// Füllt den Vorrat an ungesehenen Folgen wieder auf.
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import HaloDownloads
|
||||
import HaloPlayback
|
||||
import FlaroDownloads
|
||||
import FlaroPlayback
|
||||
import MediaKit
|
||||
|
||||
/// Übersetzt Titel vom Server in Download-Aufträge.
|
||||
@@ -15,7 +15,7 @@ import Observation
|
||||
/// Umschalten ja, nebeneinander nein.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class HaloSession {
|
||||
final class FlaroSession {
|
||||
enum Phase: Equatable {
|
||||
/// Beim Start: liegt ein Token im Schlüsselbund?
|
||||
case launching
|
||||
@@ -32,11 +32,11 @@ final class HaloSession {
|
||||
}
|
||||
|
||||
private enum Key {
|
||||
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"
|
||||
static let jellyfinAddress = "flaro.jellyfin.address"
|
||||
static let jellyfinUserID = "flaro.jellyfin.userID"
|
||||
static let jellyfinUserName = "flaro.jellyfin.userName"
|
||||
static let jellyfinServerName = "flaro.jellyfin.serverName"
|
||||
static let jellyfinIsAdministrator = "flaro.jellyfin.isAdministrator"
|
||||
}
|
||||
|
||||
/// Unter welchem Namen das Server-Token im Schlüsselbund liegt.
|
||||
@@ -55,14 +55,14 @@ final class HaloSession {
|
||||
/// könnte. Deshalb wird einmalig aufgeräumt, bevor die Namen endgültig
|
||||
/// verschwinden.
|
||||
private enum Altlast {
|
||||
static let merker = "halo.migration.plexPurged"
|
||||
static let merker = "flaro.migration.plexPurged"
|
||||
static let tokenKonten = ["plex.account", "plex.server"]
|
||||
static let schlüssel = [
|
||||
"halo.backend.kind",
|
||||
"halo.server.clientIdentifier",
|
||||
"halo.server.connectionURI",
|
||||
"halo.server.name",
|
||||
"halo.server.connectionIsLocal",
|
||||
"flaro.backend.kind",
|
||||
"flaro.server.clientIdentifier",
|
||||
"flaro.server.connectionURI",
|
||||
"flaro.server.name",
|
||||
"flaro.server.connectionIsLocal",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ final class HaloSession {
|
||||
/// Wer angemeldet ist — sofern der Server einen Namen dafür kennt.
|
||||
var accountName: String? { jellyfinAccount?.userName }
|
||||
|
||||
/// Die Mediatheken, die Halo darstellen kann — Musik und Fotos
|
||||
/// Die Mediatheken, die Flaro darstellen kann — Musik und Fotos
|
||||
/// erscheinen gar nicht erst.
|
||||
private(set) var libraries: [MediaLibrary] = []
|
||||
|
||||
@@ -233,14 +233,14 @@ final class HaloSession {
|
||||
/// Meldet sich an der Attrappe aus `Tools/JellyfinStub` an.
|
||||
///
|
||||
/// Nur in der Entwicklung und nur auf ausdrückliches Kommando:
|
||||
/// `-HaloStubServer http://127.0.0.1:8096` als Startargument. Der Weg
|
||||
/// `-FlaroStubServer 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: "HaloStubServer"),
|
||||
guard let raw = defaults.string(forKey: "FlaroStubServer"),
|
||||
let baseURL = URL(string: raw)
|
||||
else { return false }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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 HaloAspectRatio.poster }
|
||||
return compact ? HaloAspectRatio.heroCompact : HaloAspectRatio.hero
|
||||
if isPortrait(placement, compact: compact) { return FlaroAspectRatio.poster }
|
||||
return compact ? FlaroAspectRatio.heroCompact : FlaroAspectRatio.hero
|
||||
}
|
||||
|
||||
static func backdropURL(
|
||||
@@ -12,15 +12,15 @@ import MediaKit
|
||||
@Observable
|
||||
final class HomeLayoutSettings {
|
||||
private enum Key {
|
||||
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"
|
||||
static let hidden = "flaro.home.hiddenHubs"
|
||||
static let order = "flaro.home.hubOrder"
|
||||
static let heroCount = "flaro.home.heroCount"
|
||||
static let heroSource = "flaro.home.heroSource"
|
||||
static let heroLibraries = "flaro.home.heroLibraries"
|
||||
static let combinesContinueRows = "flaro.home.combinesContinueRows"
|
||||
static let hidesWatchedInLatest = "flaro.home.hidesWatchedInLatest"
|
||||
static let cardScale = "flaro.home.cardScale"
|
||||
static let heroHeight = "flaro.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 `HaloSession.Phase`: die
|
||||
/// Eigenes Modell statt eines weiteren Falls in `FlaroSession.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 Halo dabei nie —
|
||||
/// angemeldeten Client bestätigt; das Passwort erreicht Flaro 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 HaloPlayback
|
||||
import FlaroPlayback
|
||||
|
||||
/// 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 = "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"
|
||||
static let audio = "flaro.playback.audioLanguage"
|
||||
static let subtitles = "flaro.playback.subtitleLanguage"
|
||||
static let fallbacks = "flaro.languages.fallbacks"
|
||||
static let avoidsAudioDescription = "flaro.languages.avoidsAudioDescription"
|
||||
static let perLibrary = "flaro.languages.library"
|
||||
static let perSeries = "flaro.languages.series"
|
||||
static let rememberedTracks = "flaro.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 HaloDesign, das `MediaItem` gar nicht kennt.
|
||||
/// übrigen Modellen und nicht in FlaroDesign, 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 = "halo.navigation.hidden"
|
||||
static let order = "halo.navigation.order"
|
||||
static let hidden = "flaro.navigation.hidden"
|
||||
static let order = "flaro.navigation.order"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import HaloPlayback
|
||||
import FlaroPlayback
|
||||
|
||||
/// Hält fest, was beim letzten Abspielversuch passiert ist.
|
||||
///
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import HaloPlayback
|
||||
import FlaroPlayback
|
||||
|
||||
/// Was der Nutzer über die Wiedergabe eingestellt hat.
|
||||
///
|
||||
@@ -28,19 +28,19 @@ final class PlaybackSettings {
|
||||
}
|
||||
|
||||
private enum Key {
|
||||
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"
|
||||
static let wifiQuality = "flaro.playback.quality.wifi"
|
||||
static let cellularQuality = "flaro.playback.quality.cellular"
|
||||
static let autoSkipIntro = "flaro.playback.autoSkipIntro"
|
||||
static let autoSkipCredits = "flaro.playback.autoSkipCredits"
|
||||
static let autoPlayNext = "flaro.playback.autoPlayNext"
|
||||
static let locksLandscape = "flaro.playback.locksLandscape"
|
||||
static let rewindSeconds = "flaro.playback.rewindSeconds"
|
||||
static let forwardSeconds = "flaro.playback.forwardSeconds"
|
||||
static let defaultRate = "flaro.playback.defaultRate"
|
||||
static let remembersQuality = "flaro.playback.remembersQuality"
|
||||
static let rememberedQuality = "flaro.playback.rememberedQuality"
|
||||
static let showsBitrate = "flaro.playback.showsBitrate"
|
||||
static let lastPlaybackStruggled = "flaro.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 = "halo.search.recent"
|
||||
private static let recentKey = "flaro.search.recent"
|
||||
|
||||
private(set) var hubs: [MediaRowGroup] = []
|
||||
private(set) var isSearching = false
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import HaloPlayback
|
||||
import FlaroPlayback
|
||||
|
||||
/// Wie Untertitel aussehen sollen — gemerkt über Neustarts hinweg.
|
||||
///
|
||||
@@ -14,8 +14,8 @@ import HaloPlayback
|
||||
@Observable
|
||||
final class SubtitleAppearance {
|
||||
private enum Key {
|
||||
static let size = "halo.subtitles.size"
|
||||
static let palette = "halo.subtitles.palette"
|
||||
static let size = "flaro.subtitles.size"
|
||||
static let palette = "flaro.subtitles.palette"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
@@ -1,4 +1,4 @@
|
||||
import HaloPlayback
|
||||
import FlaroPlayback
|
||||
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 `Halo/Session/` von beiden Zielen
|
||||
// Sie liegt im geteilten Ordner, weil `Flaro/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.halo"
|
||||
static let group = "group.com.scarriffleservices.flaro"
|
||||
|
||||
/// 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 haloItemChanged: () async -> Void = {}
|
||||
@Entry var flaroItemChanged: () async -> Void = {}
|
||||
}
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloDownloads
|
||||
import FlaroDesign
|
||||
import FlaroDownloads
|
||||
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 `HaloDesign`, und das Paket ist ohne Abhängigkeiten deklariert — es
|
||||
/// in `FlaroDesign`, 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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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(\.haloItemChanged) private var itemChanged
|
||||
@Environment(\.flaroItemChanged) private var itemChanged
|
||||
/// Der Weg auf die Detailseite — siehe `MediaStack`.
|
||||
@Environment(\.haloPush) private var push
|
||||
@Environment(\.flaroPush) private var push
|
||||
|
||||
private var isMac: Bool { HaloPlatform.isMac }
|
||||
private var isMac: Bool { FlaroPlatform.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(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.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 HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
|
||||
/// Das Aktionsmenü auf einer Detailseite.
|
||||
@@ -17,7 +17,7 @@ struct ItemActionsMenu: View {
|
||||
/// Detailseite neu geladen werden sollte.
|
||||
let onChange: () async -> Void
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@State private var isWorking = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
|
||||
/// Die Detailseite eines Titels.
|
||||
@@ -10,7 +10,7 @@ import MediaKit
|
||||
struct MediaDetailView: View {
|
||||
let route: MediaRoute
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@State private var state: LoadState<MediaItem> = .loading
|
||||
|
||||
var body: some View {
|
||||
@@ -37,12 +37,12 @@ struct MediaDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(state.value?.title ?? route.title)
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.toolbar {
|
||||
if let item = state.value {
|
||||
ToolbarItem(placement: .haloTrailing) {
|
||||
ToolbarItem(placement: .flaroTrailing) {
|
||||
ItemActionsMenu(item: item) { await load() }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.background(Color.haloElevated, in: .capsule)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.background(Color.flaroElevated, in: .capsule)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
}
|
||||
+40
-40
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(PlaybackCoordinator.self) private var playback
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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: HaloSpacing.section) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.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, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
}
|
||||
|
||||
if let summary = item.summary, !summary.isEmpty {
|
||||
Text(summary)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
}
|
||||
|
||||
if !item.genres.isEmpty {
|
||||
@@ -53,13 +53,13 @@ struct PlayableDetailView: View {
|
||||
part: part,
|
||||
isExpanded: $showsTechnicalDetails
|
||||
)
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, HaloSpacing.section)
|
||||
.padding(.bottom, FlaroSpacing.section)
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
|
||||
private var currentMedia: MediaVersion? {
|
||||
@@ -104,9 +104,9 @@ struct PlayableDetailView: View {
|
||||
item.watchProgress == nil ? "hero.play" : "hero.resume",
|
||||
systemImage: "play.fill"
|
||||
)
|
||||
.font(.haloCardTitle)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.font(.flaroCardTitle)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(accent)
|
||||
@@ -118,11 +118,11 @@ struct PlayableDetailView: View {
|
||||
} label: {
|
||||
Label("detail.playFromStart", systemImage: "gobackward")
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.haloCardTitle)
|
||||
.padding(HaloSpacing.snug)
|
||||
.font(.flaroCardTitle)
|
||||
.padding(FlaroSpacing.snug)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(Color.haloTextSecondary)
|
||||
.tint(Color.flaroTextSecondary)
|
||||
}
|
||||
|
||||
DownloadButton(item: item, mediaIndex: selectedVersion)
|
||||
@@ -210,10 +210,10 @@ struct VersionPicker: View {
|
||||
@Binding var selection: Int
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.snug) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.snug) {
|
||||
Text("detail.versions")
|
||||
.font(.haloRowHeader)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroRowHeader)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
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: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
ForEach(tags, id: \.self) { tag in
|
||||
Text(tag)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.vertical, HaloSpacing.tight)
|
||||
.background(Color.haloElevated, in: .capsule)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.tight)
|
||||
.background(Color.flaroElevated, in: .capsule)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
}
|
||||
@@ -251,20 +251,20 @@ struct TagStrip: View {
|
||||
/// Die Besetzung mit Porträts.
|
||||
struct CastRow: View {
|
||||
let cast: [MediaPerson]
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
Text("detail.cast")
|
||||
.font(.haloRowHeader)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.font(.flaroRowHeader)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack(alignment: .top, spacing: HaloSpacing.regular) {
|
||||
HStack(alignment: .top, spacing: FlaroSpacing.regular) {
|
||||
ForEach(cast.prefix(20)) { role in
|
||||
VStack(spacing: HaloSpacing.snug) {
|
||||
HaloArtwork(
|
||||
VStack(spacing: FlaroSpacing.snug) {
|
||||
FlaroArtwork(
|
||||
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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if let character = role.role {
|
||||
Text(character)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
@@ -289,7 +289,7 @@ struct CastRow: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
}
|
||||
+41
-41
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloDownloads
|
||||
import FlaroDesign
|
||||
import FlaroDownloads
|
||||
import MediaKit
|
||||
|
||||
/// Die Detailseite einer Serie oder Staffel.
|
||||
@@ -10,10 +10,10 @@ import MediaKit
|
||||
struct SeriesDetailView: View {
|
||||
let item: MediaItem
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(DownloadCoordinator.self) private var downloads
|
||||
@Environment(LanguagePreferences.self) private var languages
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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: HaloSpacing.section) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.section) {
|
||||
header
|
||||
|
||||
ParentLinks(item: item)
|
||||
|
||||
if let summary = item.summary, !summary.isEmpty {
|
||||
Text(summary)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.padding(.horizontal, FlaroSpacing.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, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
}
|
||||
|
||||
childList
|
||||
|
||||
if !item.cast.isEmpty { CastRow(cast: item.cast) }
|
||||
}
|
||||
.padding(.bottom, HaloSpacing.section)
|
||||
.padding(.bottom, FlaroSpacing.section)
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
.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, HaloSpacing.section)
|
||||
ProgressView().frame(maxWidth: .infinity).padding(.vertical, FlaroSpacing.section)
|
||||
|
||||
case .failed(let message):
|
||||
Text(message)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
|
||||
case .loaded(let entries):
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.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(.haloRowHeader)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroRowHeader)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
Spacer()
|
||||
if entries.contains(where: { $0.kind == .episode }) {
|
||||
selectionControls(for: entries)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(entries) { entry in
|
||||
row(for: entry)
|
||||
Divider().overlay(Color.haloSeparator)
|
||||
Divider().overlay(Color.flaroSeparator)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,16 +207,16 @@ struct SeriesDetailView: View {
|
||||
Button {
|
||||
if gewaehlt { selection.remove(entry.id) } else { selection.insert(entry.id) }
|
||||
} label: {
|
||||
HStack(spacing: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
ChildRow(item: entry)
|
||||
Image(systemName: gewaehlt ? "checkmark.circle.fill" : "circle")
|
||||
.font(.title3)
|
||||
.foregroundStyle(gewaehlt ? accent : Color.haloTextMuted)
|
||||
.foregroundStyle(gewaehlt ? accent : Color.flaroTextMuted)
|
||||
}
|
||||
.contentShape(.rect)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.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: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.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, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.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: HaloSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
} else {
|
||||
Button("detail.select") { isSelecting = true }
|
||||
}
|
||||
}
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(accent)
|
||||
}
|
||||
@@ -288,14 +288,14 @@ struct SeriesDetailView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.font(.haloCardTitle)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.font(.flaroCardTitle)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(accent)
|
||||
.disabled(isWorking)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
HaloArtwork(
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
FlaroArtwork(
|
||||
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(.haloCardTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(2)
|
||||
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
@@ -427,14 +427,14 @@ private struct ChildRow: View {
|
||||
|
||||
if item.isWatched {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.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, HaloSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.contentShape(.rect)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
+18
-18
@@ -1,11 +1,11 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroPlayback
|
||||
import MediaKit
|
||||
|
||||
/// Die aufklappbaren technischen Angaben zu einer Datei.
|
||||
///
|
||||
/// Zeigt nicht nur, was in der Datei steckt, sondern auch, wie Halo sie
|
||||
/// Zeigt nicht nur, was in der Datei steckt, sondern auch, wie Flaro 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: HaloSpacing.snug) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.snug) {
|
||||
row("technical.playbackMethod", playbackMethodLabel)
|
||||
if let reason = rationaleLabel {
|
||||
row("technical.reason", reason)
|
||||
}
|
||||
|
||||
Divider().overlay(Color.haloSeparator).padding(.vertical, HaloSpacing.tight)
|
||||
Divider().overlay(Color.flaroSeparator).padding(.vertical, FlaroSpacing.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.haloSeparator).padding(.vertical, HaloSpacing.tight)
|
||||
Divider().overlay(Color.flaroSeparator).padding(.vertical, FlaroSpacing.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.haloSeparator).padding(.vertical, HaloSpacing.tight)
|
||||
Divider().overlay(Color.flaroSeparator).padding(.vertical, FlaroSpacing.tight)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("technical.file")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
Text(file)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, HaloSpacing.snug)
|
||||
.padding(.top, FlaroSpacing.snug)
|
||||
} label: {
|
||||
Text("technical.title")
|
||||
.font(.haloRowHeader)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroRowHeader)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
}
|
||||
.tint(Color.haloTextSecondary)
|
||||
.tint(Color.flaroTextSecondary)
|
||||
}
|
||||
|
||||
private func row(_ label: LocalizedStringKey, _ value: String) -> some View {
|
||||
HStack(alignment: .top) {
|
||||
Text(label)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloDownloads
|
||||
import FlaroDesign
|
||||
import FlaroDownloads
|
||||
import MediaKit
|
||||
|
||||
/// Der Download-Knopf auf einer Detailseite.
|
||||
@@ -25,10 +25,10 @@ struct DownloadButton: View {
|
||||
/// nicht drückt.
|
||||
var loadsDetails: Bool = false
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(DownloadCoordinator.self) private var downloads
|
||||
@Environment(LanguagePreferences.self) private var languages
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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(.haloCardTitle)
|
||||
.font(.flaroCardTitle)
|
||||
}
|
||||
}
|
||||
// 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(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.snug)
|
||||
}
|
||||
.disabled(isLoadingDetails)
|
||||
.buttonStyle(.bordered)
|
||||
.tint(existing?.state == .completed ? accent : Color.haloTextSecondary)
|
||||
.tint(existing?.state == .completed ? accent : Color.flaroTextSecondary)
|
||||
.accessibilityLabel(Text(label))
|
||||
.confirmationDialog(
|
||||
Text("downloads.quality.title"),
|
||||
@@ -247,10 +247,10 @@ extension DownloadQuality {
|
||||
struct DownloadAllButton: View {
|
||||
let container: MediaItem
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(DownloadCoordinator.self) private var downloads
|
||||
@Environment(LanguagePreferences.self) private var languages
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
@State private var isWorking = false
|
||||
@State private var summary: String?
|
||||
@@ -269,7 +269,7 @@ struct DownloadAllButton: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(Color.haloTextSecondary)
|
||||
.tint(Color.flaroTextSecondary)
|
||||
.disabled(isWorking)
|
||||
.alert(
|
||||
Text("downloads.batch.title"),
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import HaloDownloads
|
||||
import FlaroDownloads
|
||||
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 HaloDesign
|
||||
import HaloDownloads
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroDownloads
|
||||
import FlaroPlayback
|
||||
|
||||
/// 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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
@Environment(\.horizontalSizeClass) private var sizeClass
|
||||
|
||||
|
||||
@@ -24,16 +24,16 @@ struct DownloadsView: View {
|
||||
list
|
||||
}
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.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: .haloTrailing) { AccountToolbarButton() }
|
||||
ToolbarItem(placement: .flaroTrailing) { AccountToolbarButton() }
|
||||
}
|
||||
}
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
// 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.haloSurface)
|
||||
.haloRevealsTopChrome()
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
|
||||
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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
Spacer()
|
||||
Text(downloads.usedBytes.formatted(.byteCount(style: .file)))
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
}
|
||||
|
||||
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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.snug) {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.title)
|
||||
.font(.haloCardTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(1)
|
||||
if let subtitle = item.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
|
||||
if let message = item.failureMessage {
|
||||
Text(message)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloDestructive)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroDestructive)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, HaloSpacing.tight)
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.padding(.vertical, FlaroSpacing.tight)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
.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.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.accessibilityLabel(Text("downloads.available"))
|
||||
|
||||
case .queued, .waiting:
|
||||
Image(systemName: "clock")
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.accessibilityLabel(Text("downloads.queued"))
|
||||
}
|
||||
}
|
||||
|
||||
private var progress: some View {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.tight) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.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: HaloSpacing.snug) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: FlaroSpacing.snug) {
|
||||
Text(statusLine)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: HaloSpacing.snug)
|
||||
Spacer(minLength: FlaroSpacing.snug)
|
||||
if let percentage {
|
||||
Text(percentage)
|
||||
}
|
||||
}
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,28 +387,28 @@ private struct DownloadRow: View {
|
||||
private struct HoldNotice: View {
|
||||
let hold: TransferHold
|
||||
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: HaloSpacing.regular) {
|
||||
HStack(alignment: .top, spacing: FlaroSpacing.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(.haloCardTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
Text(hold == .offline
|
||||
? "downloads.hold.offline.description"
|
||||
: "downloads.hold.wifi.description")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, HaloSpacing.tight)
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.padding(.vertical, FlaroSpacing.tight)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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(\.haloAccent) private var accent
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
@Environment(FlaroSession.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: HaloSpacing.regular) {
|
||||
VStack(spacing: FlaroSpacing.regular) {
|
||||
heroSection
|
||||
// Die Höhe: fast das ganze Fenster.
|
||||
//
|
||||
@@ -189,7 +189,7 @@ struct HeroCarousel: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var isMac: Bool { HaloPlatform.isMac }
|
||||
private var isMac: Bool { FlaroPlatform.isMac }
|
||||
|
||||
/// Die beiden Pfeile links und rechts.
|
||||
///
|
||||
@@ -203,7 +203,7 @@ struct HeroCarousel: View {
|
||||
Spacer()
|
||||
arrow(symbol: "chevron.right", direction: 1)
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.horizontal, FlaroSpacing.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(HaloSpacing.regular)
|
||||
.padding(FlaroSpacing.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 ? HaloSpacing.regular : HaloSpacing.snug) {
|
||||
HStack(spacing: isEnlarged ? FlaroSpacing.regular : FlaroSpacing.snug) {
|
||||
ForEach(items.indices, id: \.self) { position in
|
||||
Capsule()
|
||||
.fill(position == currentIndex ? accent : Color.haloTextMuted.opacity(0.4))
|
||||
.fill(position == currentIndex ? accent : Color.flaroTextMuted.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, HaloSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.contentShape(.rect)
|
||||
.onContinuousHover { phase in
|
||||
guard isMac else { return }
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(PlaybackCoordinator.self) private var playback
|
||||
@Environment(QuickPlay.self) private var quickPlay
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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(.haloCardTitle)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.font(.flaroCardTitle)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(accent)
|
||||
|
||||
NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) {
|
||||
Label("hero.details", systemImage: "info.circle")
|
||||
.font(.haloCardTitle)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.font(.flaroCardTitle)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(Color.haloTextSecondary)
|
||||
.tint(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(HomeLayoutSettings.self) private var layout
|
||||
@State private var feed = HomeFeed()
|
||||
|
||||
@@ -41,17 +41,17 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
// 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("")
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
// 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.
|
||||
.haloClearToolbarBackground()
|
||||
.flaroClearToolbarBackground()
|
||||
// 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(\.haloItemChanged, { await feed.load(from: session.backend) })
|
||||
.environment(\.flaroItemChanged, { await feed.load(from: session.backend) })
|
||||
}
|
||||
|
||||
private func content(hubs: [MediaRowGroup]) -> some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: HaloSpacing.section) {
|
||||
LazyVStack(alignment: .leading, spacing: FlaroSpacing.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, HaloSpacing.section)
|
||||
.padding(.bottom, FlaroSpacing.section)
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
|
||||
/// Eine waagerechte Reihe von Titeln.
|
||||
@@ -8,7 +8,7 @@ struct MediaRow: View {
|
||||
/// Wird durchgereicht — siehe `QuickPlayTile`.
|
||||
var opensDetails: Bool = false
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.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: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
Text(row.title)
|
||||
.font(.haloRowHeader)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.font(.flaroRowHeader)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
LazyHStack(spacing: HaloSpacing.regular) {
|
||||
LazyHStack(spacing: FlaroSpacing.regular) {
|
||||
ForEach(row.items) { item in
|
||||
QuickPlayTile(item: item, opensDetails: opensDetails) { card(for: item) }
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.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 HaloDesign
|
||||
import FlaroDesign
|
||||
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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
|
||||
/// Was gerade gezeigt wird.
|
||||
///
|
||||
@@ -36,7 +36,7 @@ struct LibraryBrowser: View {
|
||||
|
||||
var body: some View {
|
||||
inhalt
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
// **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 { HaloSidebar.surface(in: .capsule) }
|
||||
.background { FlaroSidebar.surface(in: .capsule) }
|
||||
.clipShape(.capsule)
|
||||
.shadow(color: .black.opacity(0.45), radius: 18, y: 6)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.bottom, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.bottom, FlaroSpacing.snug)
|
||||
}
|
||||
|
||||
private var reihe: some View {
|
||||
@@ -99,8 +99,8 @@ struct LibraryBrowser: View {
|
||||
pille(eintrag)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.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(.haloCaption)
|
||||
.foregroundStyle(gewaehlt ? Color.haloTextPrimary : Color.haloTextSecondary)
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(gewaehlt ? Color.flaroTextPrimary : Color.flaroTextSecondary)
|
||||
.padding(.horizontal, FlaroSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.background {
|
||||
if gewaehlt { Capsule().fill(Color.haloElevated) }
|
||||
if gewaehlt { Capsule().fill(Color.flaroElevated) }
|
||||
}
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
|
||||
/// Filter und Sortierung einer Mediathek.
|
||||
@@ -14,8 +14,8 @@ struct LibraryFilterSheet: View {
|
||||
let library: MediaLibrary
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
@Environment(FlaroSession.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)
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("filter.title"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("action.cancel") { dismiss() }
|
||||
@@ -109,7 +109,7 @@ struct LibraryFilterSheet: View {
|
||||
}
|
||||
.task { await loadGenres() }
|
||||
}
|
||||
.haloResizableSheet()
|
||||
.flaroResizableSheet()
|
||||
}
|
||||
|
||||
private func loadGenres() async {
|
||||
@@ -1,16 +1,16 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
|
||||
/// Eine Mediathek zum Durchblättern.
|
||||
struct LibraryView: View {
|
||||
let library: MediaLibrary
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.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("halo.library.layout") private var layout: LibraryLayout = .grid
|
||||
@AppStorage("flaro.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.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(library.title)
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.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(\.haloItemChanged, { await reload() })
|
||||
.environment(\.flaroItemChanged, { 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: HaloSpacing.regular)],
|
||||
spacing: HaloSpacing.loose
|
||||
columns: [GridItem(.adaptive(minimum: 110 * sizing.cardScale), spacing: FlaroSpacing.regular)],
|
||||
spacing: FlaroSpacing.loose
|
||||
) {
|
||||
ForEach(items) { item in
|
||||
QuickPlayTile(item: item) {
|
||||
@@ -119,12 +119,12 @@ struct LibraryView: View {
|
||||
PlaceholderCard(index: position)
|
||||
}
|
||||
}
|
||||
.padding(HaloSpacing.screenMargin)
|
||||
.padding(FlaroSpacing.screenMargin)
|
||||
|
||||
moreButton.padding(.bottom, HaloSpacing.section)
|
||||
footerCount.padding(.bottom, HaloSpacing.section)
|
||||
moreButton.padding(.bottom, FlaroSpacing.section)
|
||||
footerCount.padding(.bottom, FlaroSpacing.section)
|
||||
}
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
@@ -137,19 +137,19 @@ struct LibraryView: View {
|
||||
.buttonStyle(.plain)
|
||||
.task { await loadMoreIfNeeded(reaching: item) }
|
||||
|
||||
Divider().overlay(Color.haloSeparator)
|
||||
Divider().overlay(Color.flaroSeparator)
|
||||
}
|
||||
}
|
||||
|
||||
if pendingCount > 0 {
|
||||
ProgressView()
|
||||
.padding(.vertical, HaloSpacing.section)
|
||||
.padding(.vertical, FlaroSpacing.section)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
moreButton.padding(.vertical, HaloSpacing.section)
|
||||
footerCount.padding(.bottom, HaloSpacing.section)
|
||||
moreButton.padding(.vertical, FlaroSpacing.section)
|
||||
footerCount.padding(.bottom, FlaroSpacing.section)
|
||||
}
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
|
||||
/// 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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
@@ -183,10 +183,10 @@ struct LibraryView: View {
|
||||
@ViewBuilder
|
||||
private var moreButton: some View {
|
||||
if loadMoreFailed, let page, page.hasMore {
|
||||
VStack(spacing: HaloSpacing.snug) {
|
||||
VStack(spacing: FlaroSpacing.snug) {
|
||||
Text("library.loadMoreFailed")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
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: .haloTrailing) {
|
||||
ToolbarItem(placement: .flaroTrailing) {
|
||||
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: .haloTrailing) {
|
||||
ToolbarItem(placement: .flaroTrailing) {
|
||||
Button {
|
||||
Task { await reload() }
|
||||
} label: {
|
||||
@@ -228,7 +228,7 @@ struct LibraryView: View {
|
||||
.disabled(isLoadingMore)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .haloTrailing) {
|
||||
ToolbarItem(placement: .flaroTrailing) {
|
||||
Button {
|
||||
showsFilters = true
|
||||
} label: {
|
||||
@@ -239,7 +239,7 @@ struct LibraryView: View {
|
||||
.accessibilityLabel(Text("library.filters"))
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .haloTrailing) {
|
||||
ToolbarItem(placement: .flaroTrailing) {
|
||||
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: .haloTrailing) { AccountToolbarButton() }
|
||||
ToolbarItem(placement: .flaroTrailing) { AccountToolbarButton() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,11 +380,11 @@ enum LibraryLayout: String, CaseIterable {
|
||||
/// Eine Zeile in der Listenansicht.
|
||||
private struct LibraryListRow: View {
|
||||
let item: MediaItem
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
HaloArtwork(
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
FlaroArtwork(
|
||||
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(.haloCardTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(1)
|
||||
if let subtitle = item.cardSubtitle {
|
||||
Text(subtitle)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,12 +407,12 @@ private struct LibraryListRow: View {
|
||||
|
||||
if item.isWatched {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.accessibilityLabel(Text("item.watched"))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.contentShape(.rect)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
+12
-12
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
|
||||
/// Welche Sender überhaupt angezeigt werden.
|
||||
@@ -12,8 +12,8 @@ struct ChannelSelectionSheet: View {
|
||||
let channels: [LiveChannel]
|
||||
@Binding var selection: Set<String>
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
@@ -28,9 +28,9 @@ struct ChannelSelectionSheet: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("liveTV.selection"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .haloLeading) {
|
||||
ToolbarItem(placement: .flaroLeading) {
|
||||
// „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: HaloSpacing.regular) {
|
||||
HaloArtwork(
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
FlaroArtwork(
|
||||
url: session.backend?.artworkURL(path: channel.artworkPath, width: 200, height: 120),
|
||||
aspectRatio: HaloAspectRatio.channelLogo,
|
||||
aspectRatio: FlaroAspectRatio.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.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
|
||||
Text(channel.title)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: isShown(channel) ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundStyle(isShown(channel) ? accent : Color.haloTextMuted)
|
||||
.foregroundStyle(isShown(channel) ? accent : Color.flaroTextMuted)
|
||||
}
|
||||
.contentShape(.rect)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroPlayback
|
||||
import MediaKit
|
||||
|
||||
/// Live-TV mit Programmführer.
|
||||
struct LiveTVView: View {
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.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: HaloSpacing.regular) {
|
||||
VStack(spacing: FlaroSpacing.regular) {
|
||||
Button("action.retry") { Task { await load() } }
|
||||
searchTrace
|
||||
}
|
||||
@@ -107,12 +107,12 @@ struct LiveTVView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("sidebar.liveTV"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.toolbar {
|
||||
if case .available = availability, !channels.isEmpty {
|
||||
ToolbarItem(placement: .haloTrailing) {
|
||||
ToolbarItem(placement: .flaroTrailing) {
|
||||
Button {
|
||||
showsSelection = true
|
||||
} label: {
|
||||
@@ -120,7 +120,7 @@ struct LiveTVView: View {
|
||||
}
|
||||
.accessibilityLabel(Text("liveTV.selection"))
|
||||
}
|
||||
ToolbarItem(placement: .haloTrailing) {
|
||||
ToolbarItem(placement: .flaroTrailing) {
|
||||
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: HaloSpacing.snug) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.snug) {
|
||||
if let channelSource {
|
||||
Text(channelSource)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
|
||||
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.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.lineLimit(2)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
@@ -191,18 +191,18 @@ struct LiveTVView: View {
|
||||
.frame(maxHeight: 200)
|
||||
|
||||
Button {
|
||||
HaloClipboard.copy(
|
||||
FlaroClipboard.copy(
|
||||
trace
|
||||
.map { "\($0.path) → \($0.result)" }
|
||||
.joined(separator: "\n")
|
||||
)
|
||||
} label: {
|
||||
Label("diagnostics.copy", systemImage: "doc.on.doc")
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,11 +216,11 @@ struct LiveTVView: View {
|
||||
isTuning: isTuning == channel.id,
|
||||
onTune: { Task { await tune(channel) } }
|
||||
)
|
||||
Divider().overlay(Color.haloSeparator)
|
||||
Divider().overlay(Color.flaroSeparator)
|
||||
}
|
||||
}
|
||||
}
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
@@ -329,43 +329,43 @@ private struct ChannelRow: View {
|
||||
let isTuning: Bool
|
||||
let onTune: () -> Void
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.flaroAccent) 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: HaloSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.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.
|
||||
HaloArtwork(
|
||||
FlaroArtwork(
|
||||
url: session.backend?.artworkURL(path: channel.artworkPath, width: 200, height: 120),
|
||||
aspectRatio: HaloAspectRatio.channelLogo,
|
||||
aspectRatio: FlaroAspectRatio.channelLogo,
|
||||
fallbackSymbol: "tv",
|
||||
contentMode: .fit
|
||||
)
|
||||
.frame(width: 56, height: 34)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
if let number = channel.number {
|
||||
Text(number)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
Text(channel.title)
|
||||
.font(.haloCardTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
if let current {
|
||||
Text(current.title)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
|
||||
if let next {
|
||||
Text("liveTV.next \(next.startsAt.formatted(date: .omitted, time: .shortened)) \(next.title)")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
@@ -395,8 +395,8 @@ private struct ChannelRow: View {
|
||||
.foregroundStyle(accent)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.screenMargin)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.padding(.horizontal, FlaroSpacing.screenMargin)
|
||||
.padding(.vertical, FlaroSpacing.regular)
|
||||
.contentShape(.rect)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -427,43 +427,43 @@ private struct TuneFailureSheet: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
Text(failure.channel)
|
||||
.font(.haloRowHeader)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroRowHeader)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
Text(failure.reason)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
|
||||
ForEach(Array(failure.trace.enumerated()), id: \.offset) { _, line in
|
||||
Text(line)
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
Button {
|
||||
HaloClipboard.copy(failure.trace.joined(separator: "\n"))
|
||||
FlaroClipboard.copy(failure.trace.joined(separator: "\n"))
|
||||
} label: {
|
||||
Label("diagnostics.copy", systemImage: "doc.on.doc")
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.padding(HaloSpacing.screenMargin)
|
||||
.padding(FlaroSpacing.screenMargin)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("liveTV.tuneFailed"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("action.close") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.haloResizableSheet()
|
||||
.flaroResizableSheet()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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.haloSeparator)
|
||||
Divider().overlay(Color.flaroSeparator)
|
||||
|
||||
ScrollView(.vertical) {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
@@ -65,7 +65,7 @@ struct ProgramGuide: View {
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
// 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, HaloSpacing.snug)
|
||||
.padding(.leading, FlaroSpacing.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.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.frame(width: layout.tickWidth, alignment: .leading)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ struct ProgramGuide: View {
|
||||
.clipped()
|
||||
}
|
||||
.frame(height: axisHeight)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
}
|
||||
|
||||
/// 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: HaloSpacing.snug) {
|
||||
HaloArtwork(url: artwork(channel), aspectRatio: 1, fallbackSymbol: "tv")
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
FlaroArtwork(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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(2)
|
||||
if let number = channel.number {
|
||||
Text(number)
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ struct ProgramGuide: View {
|
||||
ProgressView().controlSize(.mini)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.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.haloElevated.opacity(0.4))
|
||||
.background(Color.flaroElevated.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.padding(.horizontal, FlaroSpacing.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.haloSeparator)
|
||||
.fill(Color.flaroSeparator)
|
||||
.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Text(program.startsAt.formatted(date: .omitted, time: .shortened))
|
||||
.font(.system(.caption2, design: .rounded))
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.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.haloElevated,
|
||||
in: .rect(cornerRadius: HaloRadius.control)
|
||||
running ? accent.opacity(0.28) : Color.flaroElevated,
|
||||
in: .rect(cornerRadius: FlaroRadius.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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
HaloArtwork(url: artworkURL, aspectRatio: 1, fallbackSymbol: "tv")
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
FlaroArtwork(url: artworkURL, aspectRatio: 1, fallbackSymbol: "tv")
|
||||
.frame(width: 44, height: 44)
|
||||
.clipShape(.rect(cornerRadius: 6))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(channel.title)
|
||||
.font(.haloCardTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
Text("liveTV.timeRange \(program.startsAt.formatted(date: .omitted, time: .shortened)) \(program.endsAt.formatted(date: .omitted, time: .shortened))")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Text(program.title)
|
||||
.font(.haloRowHeader)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroRowHeader)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
if let subtitle = program.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
|
||||
if isRunning {
|
||||
Button { onTune(); dismiss() } label: {
|
||||
Label("liveTV.tune", systemImage: "play.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
} else if onRecord != nil || onRecordSeries != nil {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
if let onRecord {
|
||||
Button { onRecord(); dismiss() } label: {
|
||||
Label("liveTV.record", systemImage: "record.circle")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.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, HaloSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
@@ -373,20 +373,20 @@ private struct ProgramDetailSheet: View {
|
||||
|
||||
if let summary = program.summary, !summary.isEmpty {
|
||||
Text(summary)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
.padding(HaloSpacing.screenMargin)
|
||||
.padding(FlaroSpacing.screenMargin)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("action.close") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.haloResizableSheet()
|
||||
.flaroResizableSheet()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
|
||||
@State private var recordings: [LiveRecording] = []
|
||||
@State private var timers: [LiveTimer] = []
|
||||
@@ -47,7 +47,7 @@ struct RecordingsView: View {
|
||||
list
|
||||
}
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.task { await load() }
|
||||
.refreshable { await load() }
|
||||
.confirmationDialog(
|
||||
@@ -90,7 +90,7 @@ struct RecordingsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
} header: {
|
||||
Text("liveTV.recordings")
|
||||
}
|
||||
@@ -118,23 +118,23 @@ struct RecordingsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
} header: {
|
||||
Text("liveTV.scheduled")
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
|
||||
// MARK: - Zeilen
|
||||
|
||||
private func row(_ recording: LiveRecording) -> some View {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
HaloArtwork(
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
FlaroArtwork(
|
||||
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(.haloBody)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: HaloSpacing.tight) {
|
||||
HStack(spacing: FlaroSpacing.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloDestructive)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroDestructive)
|
||||
}
|
||||
if let subtitle = subtitle(for: recording) {
|
||||
Text(subtitle)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.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(.haloBody)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(1)
|
||||
if let subtitle = subtitle(channel: timer.channelName, start: timer.startsAt) {
|
||||
Text(subtitle)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func seriesRow(_ timer: LiveSeriesTimer) -> some View {
|
||||
HStack(spacing: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
Image(systemName: "rectangle.stack.badge.record")
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(timer.name)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(1)
|
||||
Text(timer.recordsAnyChannel ? "liveTV.anyChannel" : "liveTV.series")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 `HaloSession` —, und sie
|
||||
/// **Eine** Stelle. Es gab davon zwei — hier und in `FlaroSession` —, 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 HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Der erste Buchstabe auf einer Scheibe.
|
||||
///
|
||||
@@ -16,7 +16,7 @@ struct AccountAvatar: View {
|
||||
let name: String
|
||||
var diameter: CGFloat = 26
|
||||
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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.haloCanvas)
|
||||
.foregroundStyle(Color.flaroCanvas)
|
||||
.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 haloOpenSettings: () -> Void = {}
|
||||
@Entry var flaroOpenSettings: () -> 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(HaloSession.self) private var session
|
||||
@Environment(\.haloOpenSettings) private var openSettings
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.flaroOpenSettings) private var openSettings
|
||||
|
||||
var body: some View {
|
||||
Button(action: openSettings) {
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Wer angemeldet ist — unten in der Seitenleiste.
|
||||
///
|
||||
@@ -22,7 +22,7 @@ import HaloDesign
|
||||
struct AccountBar: View {
|
||||
@Binding var selection: SidebarDestination
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.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.haloSurface)
|
||||
.background(Color.flaroSurface)
|
||||
// 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: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
AccountAvatar(name: title)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(title)
|
||||
.font(.haloCardTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.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.haloTextMuted)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.contentShape(.rect)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -121,18 +121,18 @@ struct AccountBar: View {
|
||||
) -> some View {
|
||||
Button(action: perform) {
|
||||
Label(title, systemImage: symbol)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(isDestructive ? Color.haloDestructive : Color.haloTextPrimary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(isDestructive ? Color.flaroDestructive : Color.flaroTextPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.regular)
|
||||
.contentShape(.rect)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var separator: some View {
|
||||
Divider().overlay(Color.haloSeparator)
|
||||
Divider().overlay(Color.flaroSeparator)
|
||||
}
|
||||
|
||||
}
|
||||
+69
-69
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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 HaloSidebar: View {
|
||||
struct FlaroSidebar: 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 HaloSidebar: View {
|
||||
@Binding var placement: NavigationPlacement
|
||||
let layout: NavigationLayoutSettings
|
||||
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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 HaloSidebar: View {
|
||||
static func surface(in shape: some InsettableShape) -> some View {
|
||||
shape
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay { shape.fill(Color.haloCanvas.opacity(0.55)) }
|
||||
.overlay { shape.fill(Color.flaroCanvas.opacity(0.55)) }
|
||||
.overlay {
|
||||
shape.strokeBorder(
|
||||
LinearGradient(
|
||||
@@ -134,10 +134,10 @@ struct HaloSidebar: View {
|
||||
// Ziehen zum Umsortieren mit, und die scrollt von sich aus.
|
||||
if isEditing {
|
||||
Text("sidebar.editHint")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.bottom, HaloSpacing.snug)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.bottom, FlaroSpacing.snug)
|
||||
|
||||
List {
|
||||
ForEach(groups) { group in
|
||||
@@ -149,8 +149,8 @@ struct HaloSidebar: View {
|
||||
} header: {
|
||||
if let title = group.title {
|
||||
Text(title)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -162,13 +162,13 @@ struct HaloSidebar: View {
|
||||
.environment(\.editMode, .constant(.active))
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
ForEach(groups) { group in
|
||||
section(visible(group))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.bottom, HaloSpacing.regular)
|
||||
.padding(.horizontal, FlaroSpacing.snug)
|
||||
.padding(.bottom, FlaroSpacing.regular)
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.scrollIndicators(.never)
|
||||
@@ -176,14 +176,14 @@ struct HaloSidebar: View {
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Divider().overlay(Color.haloSeparator)
|
||||
Divider().overlay(Color.flaroSeparator)
|
||||
AccountBar(selection: $selection)
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
}
|
||||
.frame(width: Self.width)
|
||||
.background { Self.surface(in: .rect(cornerRadius: HaloRadius.panel, style: .continuous)) }
|
||||
.clipShape(.rect(cornerRadius: HaloRadius.panel, style: .continuous))
|
||||
.background { Self.surface(in: .rect(cornerRadius: FlaroRadius.panel, style: .continuous)) }
|
||||
.clipShape(.rect(cornerRadius: FlaroRadius.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 HaloSidebar: View {
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(spacing: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
Button {
|
||||
withAnimation(.snappy(duration: 0.25)) { isEditing.toggle() }
|
||||
} label: {
|
||||
Text(isEditing ? "action.done" : "sidebar.edit")
|
||||
.font(.haloCaption)
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.vertical, HaloSpacing.tight)
|
||||
.font(.flaroCaption)
|
||||
.padding(.horizontal, FlaroSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.tight)
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(isEditing ? accent : Color.haloTextSecondary)
|
||||
.foregroundStyle(isEditing ? accent : Color.flaroTextSecondary)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
@@ -214,21 +214,21 @@ struct HaloSidebar: View {
|
||||
placement = .top
|
||||
} label: {
|
||||
Image(systemName: "rectangle.topthird.inset.filled")
|
||||
.font(.haloRowHeader)
|
||||
.padding(HaloSpacing.tight)
|
||||
.font(.flaroRowHeader)
|
||||
.padding(FlaroSpacing.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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.accessibilityLabel(Text("sidebar.moveToTop"))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.top, HaloSpacing.regular)
|
||||
.padding(.bottom, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.top, FlaroSpacing.regular)
|
||||
.padding(.bottom, FlaroSpacing.snug)
|
||||
}
|
||||
|
||||
/// Die Einträge einer Gruppe beim Bearbeiten: alle, in der gespeicherten
|
||||
@@ -271,16 +271,16 @@ struct HaloSidebar: View {
|
||||
} label: {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.rotationEffect(.degrees(collapsed.contains(group.id) ? -90 : 0))
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.top, HaloSpacing.snug)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.top, FlaroSpacing.snug)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -301,12 +301,12 @@ struct HaloSidebar: View {
|
||||
private func editRow(_ entry: NavigationEntry) -> some View {
|
||||
let isHidden = layout.isHidden(entry.arrangementID)
|
||||
|
||||
return HStack(spacing: HaloSpacing.snug) {
|
||||
return HStack(spacing: FlaroSpacing.snug) {
|
||||
Button {
|
||||
layout.setHidden(!isHidden, for: entry.arrangementID)
|
||||
} label: {
|
||||
Image(systemName: isHidden ? "circle" : "checkmark.circle.fill")
|
||||
.foregroundStyle(isHidden ? Color.haloTextMuted : accent)
|
||||
.foregroundStyle(isHidden ? Color.flaroTextMuted : accent)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -318,8 +318,8 @@ struct HaloSidebar: View {
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(isHidden ? Color.haloTextMuted : Color.haloTextPrimary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(isHidden ? Color.flaroTextMuted : Color.flaroTextPrimary)
|
||||
}
|
||||
|
||||
private func row(_ entry: NavigationEntry) -> some View {
|
||||
@@ -328,23 +328,23 @@ struct HaloSidebar: View {
|
||||
return Button {
|
||||
selection = entry.destination
|
||||
} label: {
|
||||
HStack(spacing: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
Image(systemName: entry.symbol)
|
||||
.font(.haloBody)
|
||||
.font(.flaroBody)
|
||||
.frame(width: 22, alignment: .center)
|
||||
entry.title
|
||||
.font(.haloBody)
|
||||
.font(.flaroBody)
|
||||
// Ein langer Mediathekenname darf die Leiste nicht
|
||||
// breiter machen; sie hat eine feste Breite.
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.foregroundStyle(isSelected ? accent : Color.haloTextPrimary)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.foregroundStyle(isSelected ? accent : Color.flaroTextPrimary)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.background {
|
||||
if isSelected {
|
||||
Capsule().fill(Color.haloElevated)
|
||||
Capsule().fill(Color.flaroElevated)
|
||||
}
|
||||
}
|
||||
.contentShape(Capsule())
|
||||
@@ -360,29 +360,29 @@ struct HaloSidebar: 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 HaloTopBar: View {
|
||||
struct FlaroTopBar: View {
|
||||
let groups: [NavigationGroup]
|
||||
@Binding var selection: SidebarDestination
|
||||
@Binding var placement: NavigationPlacement
|
||||
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: HaloSpacing.tight) {
|
||||
HStack(spacing: FlaroSpacing.tight) {
|
||||
Button {
|
||||
placement = .sidebar
|
||||
} label: {
|
||||
Image(systemName: "sidebar.leading")
|
||||
.font(.haloBody)
|
||||
.padding(.horizontal, HaloSpacing.regular)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.font(.flaroBody)
|
||||
.padding(.horizontal, FlaroSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.accessibilityLabel(Text("sidebar.moveToSide"))
|
||||
|
||||
ForEach(groups) { group in
|
||||
@@ -395,12 +395,12 @@ struct HaloTopBar: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.background { HaloSidebar.surface(in: .capsule) }
|
||||
.padding(.horizontal, FlaroSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.background { FlaroSidebar.surface(in: .capsule) }
|
||||
.clipShape(.capsule)
|
||||
.shadow(color: .black.opacity(0.45), radius: 18, y: 6)
|
||||
.padding(.top, HaloSpacing.snug)
|
||||
.padding(.top, FlaroSpacing.snug)
|
||||
}
|
||||
|
||||
private func pill(_ entry: NavigationEntry) -> some View {
|
||||
@@ -410,13 +410,13 @@ struct HaloTopBar: View {
|
||||
selection = entry.destination
|
||||
} label: {
|
||||
entry.title
|
||||
.font(.haloBody)
|
||||
.font(.flaroBody)
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(isSelected ? accent : Color.haloTextPrimary)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.foregroundStyle(isSelected ? accent : Color.flaroTextPrimary)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.background {
|
||||
if isSelected { Capsule().fill(Color.haloElevated) }
|
||||
if isSelected { Capsule().fill(Color.flaroElevated) }
|
||||
}
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
@@ -433,21 +433,21 @@ struct HaloTopBar: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: HaloSpacing.tight) {
|
||||
HStack(spacing: FlaroSpacing.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(.haloCaption)
|
||||
Image(systemName: "chevron.down").font(.flaroCaption)
|
||||
}
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(containsSelection ? accent : Color.haloTextPrimary)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(containsSelection ? accent : Color.flaroTextPrimary)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.background {
|
||||
if containsSelection { Capsule().fill(Color.haloElevated) }
|
||||
if containsSelection { Capsule().fill(Color.flaroElevated) }
|
||||
}
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import MediaKit
|
||||
struct MediaStack<Content: View>: View {
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.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(\.haloPush) { ziel in path.append(ziel) }
|
||||
.environment(\.flaroPush) { 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 haloPush: (any Hashable) -> Void = { _ in }
|
||||
@Entry var flaroPush: (any Hashable) -> Void = { _ in }
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
import HaloDownloads
|
||||
import HaloPlayback
|
||||
import FlaroDownloads
|
||||
import FlaroPlayback
|
||||
|
||||
/// Die Wurzelansicht.
|
||||
///
|
||||
@@ -10,8 +10,8 @@ import HaloPlayback
|
||||
/// Welche Form die Navigation danach annimmt — Seitenleiste, Leiste oben,
|
||||
/// Leiste unten —, entscheidet das System und der Nutzer, nicht diese Datei.
|
||||
struct RootView: View {
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(HaloTheme.self) private var theme
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(FlaroTheme.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("halo.navigation.customization")
|
||||
@AppStorage("flaro.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("halo.navigation.placement")
|
||||
@AppStorage("flaro.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.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
|
||||
case .signedOut:
|
||||
SignInView()
|
||||
@@ -104,7 +104,7 @@ struct RootView: View {
|
||||
library
|
||||
}
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.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 `HaloSidebar`. Diese Zeile
|
||||
// das war die Seitenleiste selbst, siehe `FlaroSidebar`. 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.
|
||||
.haloAccent(theme)
|
||||
.flaroAccent(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: `HaloDownloads` hängt bewusst nicht
|
||||
// Hier und nicht im Koordinator: `FlaroDownloads` 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("halo.tab.home")
|
||||
.customizationID("flaro.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("halo.tab.library.\(library.id)")
|
||||
.customizationID("flaro.tab.library.\(library.id)")
|
||||
}
|
||||
} header: {
|
||||
Text("sidebar.libraries")
|
||||
}
|
||||
.customizationID("halo.tab.libraries")
|
||||
.customizationID("flaro.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("halo.tab.liveTV")
|
||||
.customizationID("flaro.tab.liveTV")
|
||||
}
|
||||
} else {
|
||||
Tab(value: SidebarDestination.libraryHub) {
|
||||
@@ -328,7 +328,7 @@ struct RootView: View {
|
||||
} label: {
|
||||
tabLabel("sidebar.library", symbol: "books.vertical")
|
||||
}
|
||||
.customizationID("halo.tab.libraryHub")
|
||||
.customizationID("flaro.tab.libraryHub")
|
||||
}
|
||||
|
||||
Tab(value: SidebarDestination.downloads) {
|
||||
@@ -336,7 +336,7 @@ struct RootView: View {
|
||||
} label: {
|
||||
tabLabel("sidebar.downloads", symbol: "arrow.down.circle")
|
||||
}
|
||||
.customizationID("halo.tab.downloads")
|
||||
.customizationID("flaro.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("halo.tab.settings")
|
||||
.customizationID("flaro.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("halo.tab.search")
|
||||
.customizationID("flaro.tab.search")
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.sidebarAdaptable)
|
||||
@@ -389,7 +389,7 @@ struct RootView: View {
|
||||
if sizeClass == .regular {
|
||||
HStack(spacing: 0) {
|
||||
if placement == .sidebar {
|
||||
HaloSidebar(
|
||||
FlaroSidebar(
|
||||
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 {
|
||||
HaloTopBar(
|
||||
FlaroTopBar(
|
||||
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(\.haloOpenSettings, { showsSettings = true })
|
||||
.environment(\.flaroOpenSettings, { 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: .haloTrailing) {
|
||||
ToolbarItem(placement: .flaroTrailing) {
|
||||
Button("action.close") { showsSettings = false }
|
||||
}
|
||||
}
|
||||
@@ -515,16 +515,16 @@ struct RootView: View {
|
||||
showsSettings = true
|
||||
} label: {
|
||||
AccountAvatar(name: session.accountName ?? session.serverName, diameter: 34)
|
||||
.padding(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.snug)
|
||||
// Damit er auch über einem hellen Aufmacher steht.
|
||||
.background(.black.opacity(0.28), in: .circle)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.trailing, HaloSpacing.regular)
|
||||
.padding(.trailing, FlaroSpacing.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, HaloSpacing.snug)
|
||||
.padding(.top, FlaroSpacing.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 ? HaloSidebar.margin : Self.topBarHeight)
|
||||
return statusBarHeight + (placement == .sidebar ? FlaroSidebar.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 haloRevealsTopChrome() -> some View {
|
||||
func flaroRevealsTopChrome() -> some View {
|
||||
modifier(RevealsTopChrome())
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Ein noch nicht gebauter Bereich.
|
||||
///
|
||||
@@ -15,9 +15,9 @@ struct PlaceholderScreen: View {
|
||||
} description: {
|
||||
Text("placeholder.description")
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(destination.title)
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// 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 / HaloAspectRatio.landscape).rounded())
|
||||
return CGSize(width: width, height: (width / FlaroAspectRatio.landscape).rounded())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -105,10 +105,10 @@ struct MiniPlayer: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.clipShape(.rect(cornerRadius: HaloRadius.card))
|
||||
.clipShape(.rect(cornerRadius: FlaroRadius.card))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: HaloRadius.card)
|
||||
.strokeBorder(Color.haloSeparator, lineWidth: 0.5)
|
||||
RoundedRectangle(cornerRadius: FlaroRadius.card)
|
||||
.strokeBorder(Color.flaroSeparator, lineWidth: 0.5)
|
||||
}
|
||||
.shadow(color: .black.opacity(0.5), radius: 12, y: 4)
|
||||
.contentShape(.rect)
|
||||
@@ -158,9 +158,9 @@ struct MiniPlayer: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(HaloSpacing.tight)
|
||||
.padding(FlaroSpacing.tight)
|
||||
// Über dem Fortschrittsbalken, nicht darauf.
|
||||
.padding(.bottom, HaloSpacing.tight)
|
||||
.padding(.bottom, FlaroSpacing.tight)
|
||||
}
|
||||
|
||||
/// Ein kleiner Knopf, der auf jedem Bild lesbar bleibt.
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// 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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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 HaloDownloads
|
||||
import HaloPlayback
|
||||
import FlaroDownloads
|
||||
import FlaroPlayback
|
||||
|
||||
/// Führt einen Abspielwunsch aus.
|
||||
///
|
||||
@@ -169,10 +169,10 @@ final class PlayerModel {
|
||||
|
||||
/// Wo das Bild aufhört.
|
||||
///
|
||||
/// Liegt bei `HaloPlayback`, weil beide Engines es brauchen. Der Name
|
||||
/// Liegt bei `FlaroPlayback`, weil beide Engines es brauchen. Der Name
|
||||
/// hier bleibt, damit die Ansicht weiter `PlayerModel.VideoFit` schreiben
|
||||
/// kann.
|
||||
typealias VideoFit = HaloPlayback.VideoFit
|
||||
typealias VideoFit = FlaroPlayback.VideoFit
|
||||
|
||||
/// Bild-in-Bild hängt an genau dieser Ebene und wandert deshalb mit.
|
||||
///
|
||||
@@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloDownloads
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroDownloads
|
||||
import FlaroPlayback
|
||||
|
||||
/// Der Vollbildplayer.
|
||||
struct PlayerView: View {
|
||||
@@ -13,14 +13,14 @@ struct PlayerView: View {
|
||||
/// Nutzers statt eine Nebenwirkung der Navigation.
|
||||
let model: PlayerModel
|
||||
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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 { HaloPlatform.isMac }
|
||||
private var isMac: Bool { FlaroPlatform.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(HaloSpacing.loose)
|
||||
.padding(FlaroSpacing.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, HaloPlatform.isMac ? Self.titleBarHeight : 0)
|
||||
.padding(.top, FlaroPlatform.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(\.haloAccent, accent)
|
||||
.environment(\.flaroAccent, accent)
|
||||
}
|
||||
.sheet(isPresented: $showsQualityChoice) {
|
||||
QualitySheet(model: model)
|
||||
.environment(settings)
|
||||
.environment(\.haloAccent, accent)
|
||||
.environment(\.flaroAccent, 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(HaloSpacing.loose)
|
||||
.padding(FlaroSpacing.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: HaloSpacing.regular) {
|
||||
HStack(alignment: .top, spacing: FlaroSpacing.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(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.snug)
|
||||
}
|
||||
.accessibilityLabel(Text("player.minimize"))
|
||||
|
||||
@@ -414,17 +414,17 @@ struct PlayerView: View {
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.callout.weight(.semibold))
|
||||
.padding(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.snug)
|
||||
}
|
||||
.accessibilityLabel(Text("player.stop"))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(model.title)
|
||||
.font(.haloCardTitle)
|
||||
.font(.flaroCardTitle)
|
||||
.lineLimit(1)
|
||||
if let subtitle = model.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
.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(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.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(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.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(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.snug)
|
||||
}
|
||||
.accessibilityLabel(Text("player.rotate"))
|
||||
}
|
||||
@@ -524,7 +524,7 @@ struct PlayerView: View {
|
||||
} label: {
|
||||
Image(systemName: model.isPictureInPictureActive ? "pip.exit" : "pip.enter")
|
||||
.font(.title3)
|
||||
.padding(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.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(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.snug)
|
||||
.accessibilityLabel(Text("player.airPlay"))
|
||||
}
|
||||
|
||||
@@ -554,7 +554,7 @@ struct PlayerView: View {
|
||||
} label: {
|
||||
Image(systemName: symbol)
|
||||
.font(.title3)
|
||||
.padding(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.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(.haloCaption.weight(.semibold))
|
||||
.font(.flaroCaption.weight(.semibold))
|
||||
.monospacedDigit()
|
||||
.padding(.horizontal, HaloSpacing.snug)
|
||||
.padding(.vertical, HaloSpacing.tight)
|
||||
.padding(.horizontal, FlaroSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.tight)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: HaloRadius.card)
|
||||
RoundedRectangle(cornerRadius: FlaroRadius.card)
|
||||
.strokeBorder(.white.opacity(0.5), lineWidth: 1)
|
||||
}
|
||||
.padding(HaloSpacing.snug)
|
||||
.padding(FlaroSpacing.snug)
|
||||
}
|
||||
}
|
||||
.disabled(model.isChangingQuality)
|
||||
@@ -594,7 +594,7 @@ struct PlayerView: View {
|
||||
}
|
||||
|
||||
private func transport(for model: PlayerModel) -> some View {
|
||||
HStack(spacing: HaloSpacing.section * 2) {
|
||||
HStack(spacing: FlaroSpacing.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: HaloSpacing.tight) {
|
||||
VStack(spacing: FlaroSpacing.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(.haloCardTitle)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.font(.flaroCardTitle)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.regular)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(accent)
|
||||
@@ -706,21 +706,21 @@ struct PlayerView: View {
|
||||
model.skipActiveMarker()
|
||||
} label: {
|
||||
Text(marker.isIntro ? "player.skipIntro" : "player.skipCredits")
|
||||
.font(.haloCardTitle)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.font(.flaroCardTitle)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.regular)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.white)
|
||||
.transition(.move(edge: .trailing).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.section)
|
||||
.padding(.horizontal, FlaroSpacing.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 : HaloSpacing.section) + safeArea.bottom)
|
||||
.padding(.bottom, (showsControls ? 110 : FlaroSpacing.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: HaloSpacing.regular) {
|
||||
VStack(spacing: FlaroSpacing.regular) {
|
||||
ProgressView().tint(.white)
|
||||
Text("player.restoring")
|
||||
.font(.haloCardTitle)
|
||||
.font(.flaroCardTitle)
|
||||
.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: HaloSpacing.loose) {
|
||||
VStack(spacing: FlaroSpacing.loose) {
|
||||
ProgressView().tint(.white)
|
||||
Text(model.title)
|
||||
.font(.haloCardTitle)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
Text("player.preparing")
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
}
|
||||
.padding(HaloSpacing.section)
|
||||
.padding(FlaroSpacing.section)
|
||||
}
|
||||
|
||||
private func failure(message: String) -> some View {
|
||||
VStack(spacing: HaloSpacing.loose) {
|
||||
VStack(spacing: FlaroSpacing.loose) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.white.opacity(0.8))
|
||||
Text(message)
|
||||
.font(.haloBody)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
Button("action.close") { playback.close() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(accent)
|
||||
}
|
||||
.padding(HaloSpacing.section)
|
||||
.padding(FlaroSpacing.section)
|
||||
}
|
||||
|
||||
/// Die beiden Flächen zum Springen.
|
||||
@@ -821,7 +821,7 @@ struct PlayerView: View {
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.system(size: 44))
|
||||
.foregroundStyle(.white)
|
||||
.padding(HaloSpacing.section)
|
||||
.padding(FlaroSpacing.section)
|
||||
.background(.black.opacity(0.35), in: .circle)
|
||||
.transition(.opacity.combined(with: .scale))
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroPlayback
|
||||
|
||||
/// Die Qualitätswahl im Player, samt dem, was gerade tatsächlich läuft.
|
||||
///
|
||||
@@ -11,7 +11,7 @@ import HaloPlayback
|
||||
struct QualitySheet: View {
|
||||
let model: PlayerModel
|
||||
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("action.done") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.haloResizableSheet()
|
||||
.flaroResizableSheet()
|
||||
}
|
||||
|
||||
/// Was gerade läuft — gemessen, nicht gewünscht.
|
||||
@@ -158,11 +158,11 @@ struct QualitySheet: View {
|
||||
} label: {
|
||||
HStack {
|
||||
Text(label(for: option))
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
if let detail {
|
||||
Text(detail)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
Spacer()
|
||||
if option == model.quality {
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroPlayback
|
||||
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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
} header: {
|
||||
Text("player.rememberedTrack")
|
||||
} footer: {
|
||||
@@ -121,23 +121,23 @@ struct TrackPickerSheet: View {
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text(kind.title))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("action.close") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.haloMediumSheet()
|
||||
.flaroMediumSheet()
|
||||
}
|
||||
|
||||
private func row(title: String, isSelected: Bool, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
Spacer()
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark").foregroundStyle(accent)
|
||||
@@ -146,7 +146,7 @@ struct TrackPickerSheet: View {
|
||||
.contentShape(.rect)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
.accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
import HaloPlayback
|
||||
import FlaroPlayback
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
|
||||
@State private var text = ""
|
||||
@State private var feed = SearchFeed()
|
||||
@@ -23,9 +23,9 @@ struct SearchView: View {
|
||||
results
|
||||
}
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("search.title"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.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(\.haloItemChanged, { await feed.search(text, from: session.backend) })
|
||||
.environment(\.flaroItemChanged, { await feed.search(text, from: session.backend) })
|
||||
}
|
||||
|
||||
private var results: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: HaloSpacing.section) {
|
||||
LazyVStack(alignment: .leading, spacing: FlaroSpacing.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, HaloSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.loose)
|
||||
}
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -72,7 +72,7 @@ struct SearchView: View {
|
||||
text = entry
|
||||
} label: {
|
||||
Label(entry, systemImage: "clock.arrow.circlepath")
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -81,12 +81,12 @@ struct SearchView: View {
|
||||
Button("search.forgetRecent", role: .destructive) {
|
||||
feed.forgetRecent()
|
||||
}
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.haloRevealsTopChrome()
|
||||
.flaroRevealsTopChrome()
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-28
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Die Farbeinstellungen.
|
||||
///
|
||||
@@ -8,22 +8,22 @@ import HaloDesign
|
||||
/// Farbe greift die Kontrastkorrektur — und sagt es auch, statt die Farbe
|
||||
/// stillschweigend zu verändern.
|
||||
struct AppearanceView: View {
|
||||
@Environment(HaloTheme.self) private var theme
|
||||
@Environment(FlaroTheme.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: HaloSpacing.regular)]
|
||||
private let columns = [GridItem(.adaptive(minimum: 64), spacing: FlaroSpacing.regular)]
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
LazyVGrid(columns: columns, spacing: HaloSpacing.regular) {
|
||||
LazyVGrid(columns: columns, spacing: FlaroSpacing.regular) {
|
||||
ForEach(AccentOption.palette) { option in
|
||||
swatch(option)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
} header: {
|
||||
Text("appearance.accent")
|
||||
} footer: {
|
||||
@@ -38,7 +38,7 @@ struct AppearanceView: View {
|
||||
|
||||
LabeledContent {
|
||||
Text(layout.cardScale.formatted(.percent.precision(.fractionLength(0))))
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.monospacedDigit()
|
||||
} label: {
|
||||
Text("appearance.cardScale")
|
||||
@@ -52,7 +52,7 @@ struct AppearanceView: View {
|
||||
} footer: {
|
||||
Text("appearance.cardScaleExplanation")
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
|
||||
Section {
|
||||
Toggle("appearance.customColor", isOn: $showsCustomField.animation(.snappy))
|
||||
@@ -60,7 +60,7 @@ struct AppearanceView: View {
|
||||
if showsCustomField {
|
||||
HStack {
|
||||
TextField("appearance.hexPlaceholder", text: $customHex)
|
||||
.haloPlainEntry()
|
||||
.flaroPlainEntry()
|
||||
.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloDestructive)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroDestructive)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,11 +80,11 @@ struct AppearanceView: View {
|
||||
// als im Farbwähler, und niemand wüsste warum.
|
||||
Label {
|
||||
Text("appearance.adjusted \(theme.accent.hexString)")
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
} icon: {
|
||||
Image(systemName: "info.circle")
|
||||
}
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,11 @@ struct AppearanceView: View {
|
||||
PreviewRow()
|
||||
}
|
||||
}
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("appearance.title"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.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.haloCanvas)
|
||||
.foregroundStyle(Color.flaroCanvas)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
Circle()
|
||||
.strokeBorder(
|
||||
theme.selection == option ? Color.haloTextPrimary : .clear,
|
||||
theme.selection == option ? Color.flaroTextPrimary : .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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
Label("hero.play", systemImage: "play.fill")
|
||||
.font(.haloCardTitle)
|
||||
.padding(.horizontal, HaloSpacing.loose)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.background(accent, in: .rect(cornerRadius: HaloRadius.control))
|
||||
.foregroundStyle(Color.haloCanvas)
|
||||
.font(.flaroCardTitle)
|
||||
.padding(.horizontal, FlaroSpacing.loose)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.background(accent, in: .rect(cornerRadius: FlaroRadius.control))
|
||||
.foregroundStyle(Color.flaroCanvas)
|
||||
|
||||
Label("sidebar.downloads", systemImage: "arrow.down.circle")
|
||||
.font(.haloCardTitle)
|
||||
.font(.flaroCardTitle)
|
||||
.foregroundStyle(accent)
|
||||
}
|
||||
|
||||
WatchProgressBar(fraction: 0.45)
|
||||
}
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// „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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
Text(title)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
Spacer(minLength: HaloSpacing.regular)
|
||||
Spacer(minLength: FlaroSpacing.regular)
|
||||
|
||||
Menu {
|
||||
// „Wie allgemein" zuerst und als Vorgabe: die meisten Ebenen
|
||||
@@ -53,11 +53,11 @@ struct AudioLanguagePicker: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: HaloSpacing.tight) {
|
||||
HStack(spacing: FlaroSpacing.tight) {
|
||||
Text(currentLabel)
|
||||
.font(.haloBody)
|
||||
.font(.flaroBody)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
}
|
||||
.foregroundStyle(accent)
|
||||
}
|
||||
+16
-16
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Was beim letzten Abspielversuch passiert ist.
|
||||
///
|
||||
@@ -7,8 +7,8 @@ import HaloDesign
|
||||
/// wurde, über welche Verbindung, und wie lange jeder Schritt gedauert hat.
|
||||
/// Ein Screenshot davon sagt mehr als jede Beschreibung.
|
||||
struct DiagnosticsView: View {
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.flaroAccent) 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(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
}
|
||||
if let beschreibung = session.connectionDescription {
|
||||
LabeledContent("settings.connection", value: beschreibung)
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
}
|
||||
} else {
|
||||
Text("diagnostics.noServer").foregroundStyle(Color.haloTextMuted)
|
||||
Text("diagnostics.noServer").foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,11 @@ struct DiagnosticsView: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("diagnostics.source")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
Text(attempt.sourceDescription)
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
HaloClipboard.copy(report(for: attempt))
|
||||
FlaroClipboard.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.haloTextMuted)
|
||||
Text("diagnostics.nothingYet").foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("diagnostics.title"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
"""
|
||||
Halo \(Branding.version) · Build \(Branding.buildStamp)
|
||||
Flaro \(Branding.version) · Build \(Branding.buildStamp)
|
||||
Verbindung: \(attempt.connection)
|
||||
Titel: \(attempt.title)
|
||||
Weg: \(attempt.method)
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloDownloads
|
||||
import FlaroDesign
|
||||
import FlaroDownloads
|
||||
import MediaKit
|
||||
|
||||
/// Was beim Herunterladen gilt.
|
||||
@@ -11,7 +11,7 @@ import MediaKit
|
||||
/// rutschen.
|
||||
struct DownloadSettingsView: View {
|
||||
@Environment(DownloadCoordinator.self) private var downloads
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
var body: some View {
|
||||
@Bindable var downloads = downloads
|
||||
@@ -73,11 +73,11 @@ struct DownloadSettingsView: View {
|
||||
}
|
||||
}
|
||||
.tint(accent)
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("sidebar.downloads"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
}
|
||||
|
||||
private var storageLimitBinding: Binding<Int64> {
|
||||
+17
-17
@@ -1,11 +1,11 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
|
||||
/// Reihen der Startseite ein- und ausblenden und umsortieren.
|
||||
struct HomeLayoutView: View {
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.flaroAccent) 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.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("homeLayout.title"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.monospacedDigit()
|
||||
} label: {
|
||||
Text("home.heroHeight")
|
||||
@@ -64,7 +64,7 @@ struct HomeLayoutView: View {
|
||||
} footer: {
|
||||
Text("home.heroExplanation")
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
|
||||
// **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.haloTextPrimary)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
}
|
||||
// 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: HaloSpacing.snug) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.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.haloDestructive)
|
||||
.foregroundStyle(Color.flaroDestructive)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("home.combineContinue", isOn: $layout.combinesContinueRows)
|
||||
Toggle("home.hideWatched", isOn: $layout.hidesWatchedInLatest)
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
@@ -126,8 +126,8 @@ struct HomeLayoutView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
} else if rows.isEmpty {
|
||||
Text("homeLayout.empty")
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -140,9 +140,9 @@ struct HomeLayoutView: View {
|
||||
}
|
||||
)) {
|
||||
Text(row.title)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
}
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
}
|
||||
.onMove { indices, destination in
|
||||
rows.move(fromOffsets: indices, toOffset: destination)
|
||||
@@ -162,7 +162,7 @@ struct HomeLayoutView: View {
|
||||
if !rows.isEmpty {
|
||||
Spacer()
|
||||
EditButton()
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
.textCase(nil)
|
||||
}
|
||||
#endif
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Welche Sprachen der Player von sich aus wählen soll.
|
||||
///
|
||||
@@ -9,7 +9,7 @@ import HaloDesign
|
||||
/// Vorliebe, die niemand äußern kann, ist keine.
|
||||
struct LanguagesView: View {
|
||||
@Environment(LanguagePreferences.self) private var settings
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
@State private var isAdding = false
|
||||
|
||||
@@ -61,11 +61,11 @@ struct LanguagesView: View {
|
||||
}
|
||||
}
|
||||
.tint(accent)
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("languages.title"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
// 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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
} else {
|
||||
ForEach(Array(settings.fallbacks.enumerated()), id: \.element) { position, code in
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
// Die Nummer, weil die Reihenfolge der ganze Zweck
|
||||
// ist — eine Liste ohne sichtbaren Rang liest sich
|
||||
// wie eine Menge.
|
||||
Text("\(position + 1)")
|
||||
.font(.haloCaption.monospacedDigit())
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption.monospacedDigit())
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.frame(minWidth: 18, alignment: .trailing)
|
||||
Text(name(of: code))
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
}
|
||||
}
|
||||
.onMove { von, nach in
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroPlayback
|
||||
|
||||
/// Alles, was die Wiedergabe betrifft.
|
||||
///
|
||||
@@ -9,7 +9,7 @@ import HaloPlayback
|
||||
/// einmal durchgeht und dann in Ruhe lässt.
|
||||
struct PlaybackSettingsView: View {
|
||||
@Environment(PlaybackSettings.self) private var settings
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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: HaloSpacing.snug) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,11 +108,11 @@ struct PlaybackSettingsView: View {
|
||||
}
|
||||
}
|
||||
.tint(accent)
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("settings.playback"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
}
|
||||
|
||||
private func rateLabel(_ rate: Double) -> String {
|
||||
+11
-11
@@ -1,11 +1,11 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
import MediaKit
|
||||
|
||||
/// Mit welchem Server gesprochen wird — und was er gerade anbietet.
|
||||
struct ServerSettingsView: View {
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.flaroAccent) 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(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
if let accountName = session.accountName {
|
||||
@@ -44,11 +44,11 @@ struct ServerSettingsView: View {
|
||||
}
|
||||
}
|
||||
.tint(accent)
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("settings.server"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
}
|
||||
|
||||
/// 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(HaloSession.self) private var session
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
@State private var phase: Phase = .idle
|
||||
|
||||
private enum Phase { case idle, running, done }
|
||||
|
||||
var body: some View {
|
||||
LabeledContent {
|
||||
HStack(spacing: HaloSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
|
||||
if session.backend?.capabilities.contains(.libraryScan) == true {
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Das farbige Symbol vor einer Einstellungszeile.
|
||||
///
|
||||
@@ -35,7 +35,7 @@ struct SettingsHeader: View {
|
||||
let explanation: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.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(.haloTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
Text(explanation)
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, HaloSpacing.snug)
|
||||
.listRowBackground(Color.haloSurface)
|
||||
.padding(.vertical, FlaroSpacing.snug)
|
||||
.listRowBackground(Color.flaroSurface)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Die Einstellungen — als Übersicht, nicht als eine lange Liste.
|
||||
///
|
||||
@@ -10,8 +10,8 @@ import HaloDesign
|
||||
///
|
||||
/// Jetzt führt jede Zeile in einen Bildschirm, der zu **einer** Frage gehört.
|
||||
struct SettingsView: View {
|
||||
@Environment(HaloSession.self) private var session
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.flaroAccent) 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.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
} 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)
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.haloRevealsTopChrome()
|
||||
.background(Color.haloCanvas)
|
||||
.flaroRevealsTopChrome()
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("sidebar.settings"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.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: HaloSpacing.regular) {
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
SettingsIcon(symbol: symbol, colour: colour)
|
||||
Text(title)
|
||||
}
|
||||
+8
-8
@@ -1,11 +1,11 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroPlayback
|
||||
|
||||
/// Wie Untertitel aussehen sollen.
|
||||
struct SubtitleAppearanceView: View {
|
||||
@Environment(SubtitleAppearance.self) private var appearance
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
|
||||
var body: some View {
|
||||
@Bindable var appearance = appearance
|
||||
@@ -51,11 +51,11 @@ struct SubtitleAppearanceView: View {
|
||||
}
|
||||
}
|
||||
.tint(accent)
|
||||
.haloForm()
|
||||
.flaroForm()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("subtitleStyle.title"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
}
|
||||
|
||||
/// Ein Beispielsatz über grauer Fläche.
|
||||
@@ -84,10 +84,10 @@ struct SubtitleAppearanceView: View {
|
||||
radius: 2,
|
||||
y: 1
|
||||
)
|
||||
.padding(HaloSpacing.regular)
|
||||
.padding(FlaroSpacing.regular)
|
||||
}
|
||||
.frame(height: 96)
|
||||
.clipShape(.rect(cornerRadius: HaloRadius.card))
|
||||
.clipShape(.rect(cornerRadius: FlaroRadius.card))
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
+7
-7
@@ -1,21 +1,21 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Während ein Weg zum Server gesucht wird.
|
||||
struct ConnectingView: View {
|
||||
let serverName: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: HaloSpacing.loose) {
|
||||
VStack(spacing: FlaroSpacing.loose) {
|
||||
ProgressView()
|
||||
Text(serverName.isEmpty
|
||||
? String(localized: "connecting.searching")
|
||||
: String(localized: "connecting.to \(serverName)"))
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ struct ConnectionFailureView: View {
|
||||
} actions: {
|
||||
Button("action.retry", action: onRetry).buttonStyle(.borderedProminent)
|
||||
Button("action.signOut", action: onSignOut)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
}
|
||||
}
|
||||
+60
-60
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
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(HaloSession.self) private var session
|
||||
@Environment(FlaroSession.self) private var session
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) 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, HaloSpacing.section)
|
||||
.padding(.vertical, HaloSpacing.section)
|
||||
.padding(.horizontal, FlaroSpacing.section)
|
||||
.padding(.vertical, FlaroSpacing.section)
|
||||
.frame(maxWidth: 420)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.navigationTitle(Text("signIn.jellyfin.title"))
|
||||
.haloInlineTitle()
|
||||
.flaroInlineTitle()
|
||||
.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: HaloSpacing.section) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloDestructive)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroDestructive)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,39 +94,39 @@ struct JellyfinSignInView: View {
|
||||
private func addressStep(model: JellyfinSignInModel) -> some View {
|
||||
@Bindable var model = model
|
||||
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
Text("signIn.jellyfin.addressPrompt")
|
||||
.font(.haloRowHeader)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroRowHeader)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
TextField("signIn.jellyfin.addressPlaceholder", text: $model.address)
|
||||
.textFieldStyle(.plain)
|
||||
.textContentType(.URL)
|
||||
.haloPlainEntry(isAddress: true)
|
||||
.flaroPlainEntry(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(HaloSpacing.regular)
|
||||
.background(Color.haloElevated, in: .rect(cornerRadius: HaloRadius.control))
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.padding(FlaroSpacing.regular)
|
||||
.background(Color.flaroElevated, in: .rect(cornerRadius: FlaroRadius.control))
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
// Ein Beispiel statt einer Erklärung: „Schema, Host, Port" sagt
|
||||
// niemandem etwas, „192.168.1.50" jedem.
|
||||
Text("signIn.jellyfin.addressHint")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
|
||||
Button {
|
||||
focusedField = nil
|
||||
Task { await model.findServer() }
|
||||
} label: {
|
||||
Text("signIn.jellyfin.connect")
|
||||
.font(.haloRowHeader)
|
||||
.font(.flaroRowHeader)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.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: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
foundServerHeader(model: model)
|
||||
|
||||
Text("signIn.jellyfin.quickConnectHint")
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
|
||||
Text(code)
|
||||
.font(.system(.largeTitle, design: .monospaced, weight: .semibold))
|
||||
.tracking(8)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
.textSelection(.enabled)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.regular)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.haloElevated, in: .rect(cornerRadius: HaloRadius.control))
|
||||
.background(Color.flaroElevated, in: .rect(cornerRadius: FlaroRadius.control))
|
||||
|
||||
HStack(spacing: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
ProgressView().controlSize(.small)
|
||||
Text("signIn.jellyfin.waitingForApproval")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
}
|
||||
|
||||
Text("signIn.jellyfin.privacy")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
|
||||
Divider().overlay(Color.haloSeparator)
|
||||
Divider().overlay(Color.flaroSeparator)
|
||||
|
||||
Button("signIn.jellyfin.usePassword") { model.useCredentials() }
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
Button("signIn.jellyfin.otherAddress") { model.startOver() }
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,12 +181,12 @@ struct JellyfinSignInView: View {
|
||||
private func credentialsStep(model: JellyfinSignInModel) -> some View {
|
||||
@Bindable var model = model
|
||||
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.regular) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.regular) {
|
||||
foundServerHeader(model: model)
|
||||
|
||||
TextField("signIn.jellyfin.userName", text: $model.userName)
|
||||
.textContentType(.username)
|
||||
.haloPlainEntry()
|
||||
.flaroPlainEntry()
|
||||
.submitLabel(.next)
|
||||
.focused($focusedField, equals: .userName)
|
||||
.onSubmit { focusedField = .password }
|
||||
@@ -200,8 +200,8 @@ struct JellyfinSignInView: View {
|
||||
focusedField = .password
|
||||
return .handled
|
||||
}
|
||||
.padding(HaloSpacing.regular)
|
||||
.background(Color.haloElevated, in: .rect(cornerRadius: HaloRadius.control))
|
||||
.padding(FlaroSpacing.regular)
|
||||
.background(Color.flaroElevated, in: .rect(cornerRadius: FlaroRadius.control))
|
||||
|
||||
SecureField("signIn.jellyfin.password", text: $model.password)
|
||||
.textContentType(.password)
|
||||
@@ -214,17 +214,17 @@ struct JellyfinSignInView: View {
|
||||
focusedField = .userName
|
||||
return .handled
|
||||
}
|
||||
.padding(HaloSpacing.regular)
|
||||
.background(Color.haloElevated, in: .rect(cornerRadius: HaloRadius.control))
|
||||
.padding(FlaroSpacing.regular)
|
||||
.background(Color.flaroElevated, in: .rect(cornerRadius: FlaroRadius.control))
|
||||
|
||||
Button {
|
||||
focusedField = nil
|
||||
Task { await model.signInWithPassword() }
|
||||
} label: {
|
||||
Text("signIn.jellyfin.submit")
|
||||
.font(.haloRowHeader)
|
||||
.font(.flaroRowHeader)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.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(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
|
||||
Divider().overlay(Color.haloSeparator)
|
||||
Divider().overlay(Color.flaroSeparator)
|
||||
|
||||
if model.offersQuickConnect {
|
||||
Button("signIn.jellyfin.useCode") { Task { await model.beginQuickConnect() } }
|
||||
.font(.haloCaption)
|
||||
.font(.flaroCaption)
|
||||
}
|
||||
Button("signIn.jellyfin.otherAddress") { model.startOver() }
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
}
|
||||
|
||||
// MARK: - Bausteine
|
||||
|
||||
@ViewBuilder
|
||||
private func foundServerHeader(model: JellyfinSignInModel) -> some View {
|
||||
VStack(alignment: .leading, spacing: HaloSpacing.tight) {
|
||||
VStack(alignment: .leading, spacing: FlaroSpacing.tight) {
|
||||
Text("signIn.jellyfin.foundServer")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.textCase(.uppercase)
|
||||
Text(model.serverName ?? model.address)
|
||||
.font(.haloTitle)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroTitle)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func waiting(text: LocalizedStringKey) -> some View {
|
||||
HStack(spacing: HaloSpacing.snug) {
|
||||
HStack(spacing: FlaroSpacing.snug) {
|
||||
ProgressView().controlSize(.small)
|
||||
Text(text)
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.vertical, HaloSpacing.section)
|
||||
.padding(.vertical, FlaroSpacing.section)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import FlaroDesign
|
||||
|
||||
/// Der Anmeldebildschirm.
|
||||
///
|
||||
@@ -12,54 +12,54 @@ import HaloDesign
|
||||
/// 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(\.haloAccent) private var accent
|
||||
@Environment(\.flaroAccent) private var accent
|
||||
@State private var showsJellyfinSignIn = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: HaloSpacing.section) {
|
||||
VStack(spacing: FlaroSpacing.section) {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: HaloSpacing.regular) {
|
||||
VStack(spacing: FlaroSpacing.regular) {
|
||||
Image(systemName: "play.rectangle.on.rectangle")
|
||||
.font(.system(size: 52))
|
||||
.foregroundStyle(accent)
|
||||
|
||||
Text(Branding.appName)
|
||||
.font(.haloHero)
|
||||
.foregroundStyle(Color.haloTextPrimary)
|
||||
.font(.flaroHero)
|
||||
.foregroundStyle(Color.flaroTextPrimary)
|
||||
|
||||
Text("signIn.tagline")
|
||||
.font(.haloBody)
|
||||
.foregroundStyle(Color.haloTextSecondary)
|
||||
.font(.flaroBody)
|
||||
.foregroundStyle(Color.flaroTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: HaloSpacing.regular) {
|
||||
VStack(spacing: FlaroSpacing.regular) {
|
||||
Button {
|
||||
showsJellyfinSignIn = true
|
||||
} label: {
|
||||
Text("signIn.withJellyfin")
|
||||
.font(.haloRowHeader)
|
||||
.font(.flaroRowHeader)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, HaloSpacing.regular)
|
||||
.padding(.vertical, FlaroSpacing.regular)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(accent)
|
||||
|
||||
Text("signIn.privacy")
|
||||
.font(.haloCaption)
|
||||
.foregroundStyle(Color.haloTextMuted)
|
||||
.font(.flaroCaption)
|
||||
.foregroundStyle(Color.flaroTextMuted)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Spacer().frame(height: HaloSpacing.section)
|
||||
Spacer().frame(height: FlaroSpacing.section)
|
||||
}
|
||||
.padding(.horizontal, HaloSpacing.section)
|
||||
.padding(.horizontal, FlaroSpacing.section)
|
||||
.frame(maxWidth: 420)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.haloCanvas)
|
||||
.background(Color.flaroCanvas)
|
||||
.sheet(isPresented: $showsJellyfinSignIn) {
|
||||
JellyfinSignInView()
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.scarriffleservices.halo</string>
|
||||
<string>group.com.scarriffleservices.flaro</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,12 +1,12 @@
|
||||
import SwiftUI
|
||||
import HaloDesign
|
||||
import HaloPlayback
|
||||
import FlaroDesign
|
||||
import FlaroPlayback
|
||||
import MediaKit
|
||||
import JellyfinKit
|
||||
|
||||
/// Die App auf dem Fernseher.
|
||||
///
|
||||
/// **Eine eigene Wurzel statt eines Zweigs in `HaloApp`.** Der Unterschied
|
||||
/// **Eine eigene Wurzel statt eines Zweigs in `FlaroApp`.** 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: `Halo/Session/`
|
||||
/// **Die Modelle dagegen werden geteilt**, nicht kopiert: `Flaro/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 HaloTVApp: App {
|
||||
@State private var session: HaloSession
|
||||
@State private var theme = HaloTheme()
|
||||
struct FlaroTVApp: App {
|
||||
@State private var session: FlaroSession
|
||||
@State private var theme = FlaroTheme()
|
||||
@State private var playback = PlaybackCoordinator()
|
||||
@State private var playbackSettings = PlaybackSettings()
|
||||
@State private var languages = LanguagePreferences()
|
||||
@@ -40,7 +40,7 @@ struct HaloTVApp: App {
|
||||
@State private var network = NetworkConditions()
|
||||
|
||||
init() {
|
||||
_session = State(initialValue: HaloSession(
|
||||
_session = State(initialValue: FlaroSession(
|
||||
jellyfinIdentity: JellyfinIdentity.current(
|
||||
client: Branding.appName,
|
||||
version: Branding.version
|
||||
@@ -62,7 +62,7 @@ struct HaloTVApp: App {
|
||||
.environment(homeLayout)
|
||||
.environment(diagnostics)
|
||||
.environment(network)
|
||||
.haloAccent(theme)
|
||||
.flaroAccent(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>Halo</string>
|
||||
<string>Flaro</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -22,10 +22,10 @@
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.scarriffleservices.halo.topshelf</string>
|
||||
<string>com.scarriffleservices.flaro.topshelf</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>halo</string>
|
||||
<string>flaro</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
@@ -39,7 +39,7 @@
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Halo benötigt Zugriff auf das lokale Netzwerk, um deinen Jellyfin-Server zu erreichen.</string>
|
||||
<string>Flaro 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