#if os(tvOS) import Foundation /// **Auf dem Fernseher gibt es kein Anmeldefenster.** /// /// `ASWebAuthenticationSession` steht im tvOS-SDK, aber fast alles daran ist /// als nicht verfügbar markiert: kein `presentationContextProvider`, kein /// `prefersEphemeralWebBrowserSession`, nicht einmal `cancel()`. Ein /// Fernseher hat auch keinen Browser, in dem sich jemand bei Plex anmelden /// würde — und Plex ist in der ersten Fassung ohnehin nicht dabei. /// /// **Der Typ bleibt trotzdem stehen.** `FlixySession` hält ihn als Feld und /// ruft ihn an sechs Stellen; ihn herauszuschneiden hiesse, die /// Zustandsmaschine der Anmeldung für eine Plattform zu spalten, die diesen /// Zweig nie betritt. Dieselbe Bauform wie bei `OrientationLock`: derselbe /// Name, dieselben Aufrufe, und nichts passiert. @MainActor final class PlexWebAuthenticator { enum Failure: Error, Equatable { case cancelled case presentationFailed(String) } /// Immer „ging nicht": wer hier landet, soll nicht warten. private(set) var outcome: Failure? private(set) var didReturn = false func present(url: URL, callbackScheme: String) { outcome = .presentationFailed(String(localized: "signIn.windowFailed")) didReturn = true } func dismiss() {} } #else import AuthenticationServices import Foundation #if canImport(UIKit) import UIKit #else import AppKit #endif /// Öffnet die Anmeldeseite von Plex in einem Fenster innerhalb der App. /// /// `ASWebAuthenticationSession` ist dafür der vorgesehene Weg: die Seite kommt /// von Plex, das Passwort wird dort eingegeben und erreicht die App nie. Nach /// dem Anmelden schickt Plex den Browser auf eine Adresse mit unserem eigenen /// Schema, das Fenster schließt sich von selbst, und die App weiß, dass sie /// den Token abholen kann. /// /// Ein einfacher `SFSafariViewController` täte es nicht: er meldet nicht, wann /// die Rückadresse erreicht wurde, und müsste von Hand geschlossen werden. @MainActor final class PlexWebAuthenticator: NSObject { enum Failure: Error, Equatable { /// Der Nutzer hat das Fenster geschlossen. Kein Fehler im eigentlichen /// Sinn — die Oberfläche soll darauf kommentarlos zurückgehen. case cancelled case presentationFailed(String) } /// Das laufende Fenster — damit es sich auch von außen schließen lässt. /// /// Nötig, weil Plex den Rücksprung nicht immer ausführt: manchmal landet /// der Browser nach dem Anmelden auf watch.plex.tv und bleibt dort stehen. /// Der Token liegt dann längst bereit, nur das Fenster weiß es nicht. private var session: ASWebAuthenticationSession? /// Wie das Fenster ausgegangen ist. /// /// `nil`, solange es noch offen ist. private(set) var outcome: Failure? private(set) var didReturn = false /// Öffnet das Anmeldefenster und kehrt **sofort** zurück. /// /// Bewusst nicht abwartend: Plex führt den Rücksprung über `forwardUrl` /// nicht zuverlässig aus. Auf dem iPad landet der Browser nach dem /// Anmelden auf watch.plex.tv und bleibt stehen — der Token liegt längst /// bereit, aber ein abwartender Aufruf hinge auf einen Rückruf, der nie /// kommt. Der Aufrufer fragt deshalb parallel beim Server nach und sieht /// hier nur nach, ob das Fenster zwischendurch geschlossen wurde. func present(url: URL, callbackScheme: String) { outcome = nil didReturn = false let session = ASWebAuthenticationSession( url: url, callbackURLScheme: callbackScheme ) { [weak self] _, error in // ASWebAuthenticationSession ruft den Abschlussblock auf dem // Hauptthread auf — anders als mancher Rückruf, wo dieselbe Annahme die App // zum Absturz gebracht hat. Hier ist sie dokumentiert. MainActor.assumeIsolated { guard let self else { return } if let error = error as? ASWebAuthenticationSessionError, error.code == .canceledLogin { self.outcome = .cancelled } else if let error { self.outcome = .presentationFailed(error.localizedDescription) } self.didReturn = true self.session = nil } } session.presentationContextProvider = self // `false`, damit eine bestehende Plex-Anmeldung in Safari mitgenutzt // wird: wer dort schon angemeldet ist, braucht nur zu bestätigen. session.prefersEphemeralWebBrowserSession = false guard session.start() else { outcome = .presentationFailed(String(localized: "signIn.windowFailed")) didReturn = true return } self.session = session } } extension PlexWebAuthenticator { /// Schließt das Anmeldefenster. /// /// Wird gerufen, sobald der Token auf anderem Weg eingetroffen ist — sonst /// bliebe der Browser mit der Plex-Seite offen stehen, obwohl die App /// schon angemeldet ist. func dismiss() { session?.cancel() session = nil } } extension PlexWebAuthenticator: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { // Das Fenster der App. Gesucht statt gehalten, weil eine SwiftUI-App // keinen festen Verweis darauf hat — und ein frisch erzeugtes Fenster // erschiene hinter dem sichtbaren. #if canImport(UIKit) let scene = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .first { $0.activationState == .foregroundActive } return scene?.keyWindow ?? ASPresentationAnchor() #else return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor() #endif } } #endif