Vollständig entfernt: das Paket `PlexKit` samt 108 Tests, der Web-Authenticator, die Anmeldung über plex.tv, die Serverauswahl und alles, was daran hing. 43 Dateien gelöscht, 68 Stellen in 28 weiteren bereinigt. **Zwei Dinge hätten beim blossen Streichen still das Falsche getan.** `playbackUnavailableMessage` deckte im Else-Zweig zwei Lagen gleichzeitig ab — „es ist Plex" und „niemand ist angemeldet". Wer die Serverart naiv herausstreicht, bekommt als nicht angemeldeter Nutzer „Jellyfin-Wiedergabe steht aus" statt „Server nicht erreichbar". Die Unterscheidung hängt jetzt daran, ob überhaupt ein Server da ist. Die Schlüsselbund-Einträge `plex.account` und `plex.server` wurden allein über den Authenticator aus dem gelöschten Paket entfernt. Danach stünde `plex.account` in keiner Zeile dieses Projekts mehr: wer sich nie abgemeldet hat, trüge ein gültiges Token am fremden Konto bis in alle Ewigkeit mit sich, ohne dass irgendein Weg es noch löschen könnte. `purgePlexLeftovers()` räumt beide Token und fünf Voreinstellungen einmalig ab — und fasst nichts an, wo nichts liegt, damit kein Jellyfin-Nutzer dafür seinen Mediathekenspeicher verliert. **Was Nutzer sehen.** Die Berechtigungsfrage fürs lokale Netz sagte wörtlich, die App brauche es, „um deinen Plex-Server zu finden" — das stand so im Systemdialog. Der Anmeldebildschirm hatte zwei Knöpfe, und der hervorgehobene war der für Plex; der verbliebene erbt die Hervorhebung, denn ein Knopf, der aussieht wie die zweite Wahl, lässt nach der ersten suchen. 14 verwaiste Textbausteine sind weg, vier umformuliert. Darunter einer, der schlicht nicht mehr stimmte: `signIn.privacy` versprach, das Passwort erreiche diese App nie. Beim Jellyfin-Rückfall auf das Passwort tut es das sehr wohl. Die Zusage, die weiterhin gilt, ist die über fremde Dienste — und nur die steht jetzt da. **Was ausdrücklich bleibt:** `MediaBackend`. Diese Abstraktion hat zwei Server gleichzeitig getragen, und der Steckplatz, den Plex räumt, ist genau der, auf den Halo kommt. `ServerKind` fällt dagegen weg — mit einem Fall ist es ein Bool, das `backend != nil` schon beantwortet. Nebenbei zog die README nach. Sie beschrieb noch einen „nativen Plex-Client" mit zwei Playern und VLCKit, beides seit Längerem nicht mehr wahr. 368 Tests grün (von 476 minus die 108 aus PlexKitTests, kein anderer). iOS und tvOS bauen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
205 lines
8.6 KiB
Swift
205 lines
8.6 KiB
Swift
import SwiftData
|
|
import SwiftUI
|
|
#if os(iOS)
|
|
import UIKit
|
|
#endif
|
|
import FlixyDesign
|
|
import FlixyDownloads
|
|
import FlixyPlayback
|
|
import JellyfinKit
|
|
import MediaKit
|
|
|
|
@main
|
|
struct FlixyApp: 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
|
|
/// eigenen Weg — nur den klassischen App-Delegaten. Auf dem Mac wird eine
|
|
/// App nicht aus dem Speicher geworfen, während sie lädt; dort gibt es
|
|
/// weder das Signal noch den Bedarf.
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
|
#endif
|
|
|
|
/// 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()
|
|
@State private var languages = LanguagePreferences()
|
|
@State private var quickPlay = QuickPlay()
|
|
@State private var subtitleAppearance = SubtitleAppearance()
|
|
/// 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 diagnostics = PlaybackDiagnostics()
|
|
@State private var downloads: DownloadCoordinator
|
|
|
|
private let modelContainer: ModelContainer
|
|
|
|
init() {
|
|
_session = State(initialValue: FlixySession(
|
|
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()
|
|
// Eine Untergrenze, unter der die Oberfläche nicht mehr
|
|
// aufgeht: schmaler als das hier klappt die Seitenleiste zu
|
|
// und die Reihen zeigen anderthalb Kacheln.
|
|
//
|
|
// Zur Laufzeit gefragt, nicht beim Übersetzen: auf dem Mac
|
|
// läuft genau dieser iOS-Bau, und `os(macOS)` ist dabei
|
|
// falsch. Ohne Untergrenze öffnet das Fenster in
|
|
// Notizzettelgröße, und darin fällt der Aufmacher in sich
|
|
// zusammen, weil seine Abstände dehnbar sind.
|
|
.frame(
|
|
minWidth: FlixyPlatform.isMac ? 900 : nil,
|
|
minHeight: FlixyPlatform.isMac ? 620 : nil
|
|
)
|
|
// `defaultSize` gibt es hier nicht; die Größe hängt an der
|
|
// Szene.
|
|
.task { Self.widenWindow() }
|
|
.environment(session)
|
|
.environment(playback)
|
|
.environment(playbackSettings)
|
|
.environment(languages)
|
|
.environment(quickPlay)
|
|
.environment(subtitleAppearance)
|
|
.environment(network)
|
|
.environment(downloads)
|
|
.environment(homeLayout)
|
|
.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 {
|
|
#if os(iOS)
|
|
appDelegate.downloads = downloads
|
|
#endif
|
|
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)
|
|
#if os(macOS)
|
|
// Ohne diese Angabe öffnet macOS ein Fenster von der Größe eines
|
|
// Notizzettels — und darin fällt der Anmeldebildschirm in sich
|
|
// zusammen, weil seine Abstände dehnbar sind.
|
|
.defaultSize(width: 1280, height: 820)
|
|
// Das Fenster darf nicht kleiner werden, als der Inhalt verträgt.
|
|
.windowResizability(.contentMinSize)
|
|
// Keine graue Titelleiste über einem dunklen Bild: die Ampelknöpfe
|
|
// schweben über dem Inhalt, wie man es von Fernseh-Apps kennt.
|
|
.windowStyle(.hiddenTitleBar)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
extension FlixyApp {
|
|
/// Gibt dem Fenster eine vernünftige Mindestgröße.
|
|
///
|
|
/// Auf dem Mac öffnet das System sonst ein Fenster von der Größe eines
|
|
/// Notizzettels — und darin fällt der Aufmacher in sich zusammen, weil
|
|
/// seine Abstände dehnbar sind.
|
|
///
|
|
/// `sizeRestrictions` gibt es nur dort, wo Fenster sich überhaupt frei
|
|
/// ziehen lassen; auf dem iPhone ist die Eigenschaft `nil`. Das
|
|
/// Fragezeichen ist deshalb die ganze Fallunterscheidung — ein Aufruf
|
|
/// auf einem Gerät ohne Fenster tut schlicht nichts.
|
|
@MainActor
|
|
static func widenWindow() {
|
|
for scene in UIApplication.shared.connectedScenes {
|
|
guard let windowScene = scene as? UIWindowScene else { continue }
|
|
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 900, height: 620)
|
|
windowScene.sizeRestrictions?.maximumSize = CGSize(
|
|
width: CGFloat.greatestFiniteMagnitude,
|
|
height: CGFloat.greatestFiniteMagnitude
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|
|
}
|
|
#endif
|