Files
Flaro/Flixy/FlixyApp.swift
T
ScarriffleandClaude Opus 5 5cebfed36b Navigation: Glas, oben oder seitlich, und mehr Reihen auf der Startseite
Drei Dinge aus dem letzten Durchgang.

**Die Startseite hatte zu wenig anzubieten.** Es gab eine Reihe „Zuletzt
hinzugefügt" für alles zusammen. Jetzt gibt es eine je Mediathek — „Neu in
Filme" und „Neu in Serien" sind zwei verschiedene Fragen, und wer nach einem
Film sucht, blättert in einer gemeinsamen Reihe an Folgen vorbei. Dazu
Favoriten, die es bei Jellyfin ohnehin gibt. Die drei Abfragen laufen
nebeneinander; nur die Mediatheken werden streng geholt, weil sie die Probe
darauf sind, ob der Server überhaupt antwortet.

**Die Seitenleiste ist jetzt echtes Glas**, nicht das alte Material: im
ausfahrbaren Blatt über `glassEffect`, in der geteilten Ansicht auf dem iPad
vom System selbst — dort lag mein eigener Hintergrund darüber und machte ihn
stumpf. Die zum Inhalt zeigende Kante ist gerundet, damit das Blatt sichtbar
über der Seite liegt statt daneben zu stehen.

**Die Navigation kann nach oben.** Eine Einstellung unter Erscheinungsbild,
seitlich oder oben. Oben stehen alle Einträge gleichzeitig da, als Glaskapseln
in einer waagerecht scrollenden Reihe — die Zahl der Mediatheken kommt vom
Server, und zusammengequetschte Beschriftungen wären die schlechtere Antwort
als eine Reihe, die weiterläuft.

Angesehen auf iPhone und iPad: beide Formen, das Glas in beiden Leisten, die
neuen Reihen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:31:09 +02:00

145 lines
5.8 KiB
Swift

import SwiftData
import SwiftUI
import FlixyDesign
import FlixyDownloads
import FlixyPlayback
import JellyfinKit
import MediaKit
import PlexKit
@main
struct FlixyApp: App {
/// Nimmt das Signal entgegen, mit dem iOS die App zum Abschluss von
/// Hintergrund-Downloads aufweckt. Dafür gibt es in SwiftUI keinen
/// eigenen Weg — nur den klassischen App-Delegaten.
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
/// Leben hier, nicht in einer View: sie gelten für die gesamte App, und
/// ein Neuaufbau der Wurzelansicht darf weder die Anmeldung noch laufende
/// Downloads zurücksetzen.
@State private var theme = FlixyTheme()
@State private var session: FlixySession
@State private var playback = PlaybackCoordinator()
@State private var playbackSettings = PlaybackSettings()
/// Beobachtet WLAN und Mobilfunk. Lebt hier, damit der Beobachter einmal
/// läuft statt bei jedem Öffnen des Players neu zu starten.
@State private var network = NetworkConditions()
@State private var homeLayout = HomeLayoutSettings()
@State private var navigation = NavigationSettings()
@State private var diagnostics = PlaybackDiagnostics()
@State private var downloads: DownloadCoordinator
private let modelContainer: ModelContainer
init() {
let identity = PlexClientIdentity.current(
product: Branding.plexProduct,
version: Branding.version
)
_session = State(initialValue: FlixySession(
identity: identity,
jellyfinIdentity: JellyfinIdentity.current(
client: Branding.appName,
version: Branding.version
),
tokenStore: KeychainTokenStore(service: Branding.keychainService)
))
// Scheitert das Anlegen der Datenbank, ist die App nicht sinnvoll zu
// benutzen — dann lieber hier abbrechen als später bei jedem Download
// stillschweigend nichts tun.
let container = try! ModelContainer(for: DownloadItem.self)
self.modelContainer = container
let fileStore = try! DownloadFileStore()
_downloads = State(initialValue: DownloadCoordinator(
container: container,
fileStore: fileStore
))
}
var body: some Scene {
WindowGroup {
RootView()
.environment(session)
.environment(playback)
.environment(playbackSettings)
.environment(network)
.environment(downloads)
.environment(homeLayout)
.environment(navigation)
.environment(diagnostics)
.environment(theme)
// Akzentfarbe und Tönung setzt RootView, nicht diese Stelle.
//
// Scene-Bodies werten Änderungen an beobachteten Objekten
// nicht zuverlässig neu aus. Stand `.tint` hier, blieb nach
// einem Farbwechsel ein Teil der Oberfläche auf dem alten
// Wert stehen — sichtbar etwa an Picker-Werten, die weiter in
// der Standardfarbe leuchteten, während die Schalter schon
// umgestellt waren.
//
// Der eigentliche Schalter für „nur dunkel" steht in der
// Info.plist (UIUserInterfaceStyle). Diese Zeile ist die
// Absicherung für SwiftUI-Vorschauen, die sie nicht lesen.
.preferredColorScheme(.dark)
.task {
appDelegate.downloads = downloads
await downloads.restore()
// Beim Start einmal prüfen: während die App weg war,
// können Folgen gesehen oder gelöscht worden sein.
await DownloadRefiller(
backend: session.backend,
coordinator: downloads
).refill()
}
}
.modelContainer(modelContainer)
}
}
/// Der klassische App-Delegat, für das eine, was SwiftUI nicht abdeckt.
@MainActor
final class AppDelegate: NSObject, UIApplicationDelegate {
weak var downloads: DownloadCoordinator?
/// Nicht `@Sendable`: UIKit reicht den Block ohne diese Zusage herein,
/// und er wird ohnehin nur auf dem Hauptakteur aufgerufen. Der Delegat
/// selbst ist an den Hauptakteur gebunden.
private var pendingCompletionHandler: (() -> Void)?
/// Welche Ausrichtungen gerade erlaubt sind.
///
/// Wird vom System bei jeder Drehung gefragt. Ohne diese Antwort dreht
/// `requestGeometryUpdate` zwar kurz, das System dreht aber sofort zurück
/// — genau das sah aus wie „wechselt nicht so richtig ins Querformat".
func application(
_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?
) -> UIInterfaceOrientationMask {
MainActor.assumeIsolated { OrientationLock.shared.mask }
}
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
// iOS hat die App im Hintergrund gestartet, weil Downloads fertig
// geworden sind. Der Abschlussblock muss aufgerufen werden, sobald
// alles verarbeitet ist — sonst beendet das System die App hart und
// merkt sich das für künftige Aufweckversuche negativ.
guard identifier == DownloadCoordinator.sessionIdentifier else {
completionHandler()
return
}
pendingCompletionHandler = completionHandler
Task { [weak self] in
await self?.downloads?.restore()
self?.pendingCompletionHandler?()
self?.pendingCompletionHandler = nil
}
}
}