Absturz beim Abspielen, und die Annahme dahinter
Der Crashlog vom Gerät:
_dispatch_assert_queue_fail
_swift_task_checkIsolatedSwift
MainActor.assumeIsolated
VLCPlayerEngine.mediaPlayerStateChanged(_:)
VLCKit input_thread_Events
Im Code stand als Begründung für assumeIsolated, VLCKit stelle seine
Rückrufe über die Hauptwarteschlange zu. Das ist falsch — es ruft den
Delegaten von seinem eigenen Input-Thread. Die App brach deshalb hart
ab, sobald VLC den ersten Zustandswechsel meldete, also unmittelbar
beim Drücken auf Play.
Alle drei Delegat-Methoden springen jetzt mit Task { @MainActor in }
sauber hinüber. Die Reihenfolge bleibt erhalten: die Aufgaben werden aus
demselben Thread nacheinander eingereiht.
Dieselbe Annahme steckte im AVPlayer-Motor in den KVO-Beobachtern —
KVO meldet auf dem Thread, der die Änderung ausgelöst hat, und AVPlayer
ändert seinen Zustand auch aus internen Threads. Ebenfalls umgestellt.
Die beiden Rückrufe mit ausdrücklichem queue: .main behalten
assumeIsolated: dort ist die Annahme belegt statt geraten, und der
Zeitbeobachter feuert viermal je Sekunde.
Dazu die Anmeldung: 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 bereit, aber die App
wartete auf einen Rückruf, der nie kam. Das Fenster wird jetzt geöffnet,
ohne darauf zu warten; parallel wird im Sekundentakt beim Server
nachgefragt. Der Rücksprung ist damit eine Annehmlichkeit, keine
Voraussetzung.
132 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -471,6 +471,222 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.connection": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbindung"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.connectionUsed": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbindung"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.copy": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Bericht kopieren"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Copy report"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.copyHint": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Enthält keinen Token. Kannst du unbesorgt weitergeben."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Contains no token. Safe to share."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.lastAttempt": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Letzter Abspielversuch"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Last playback attempt"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.method": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Weg"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Method"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.noServer": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Keine Serververbindung."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "No server connection."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.nothingYet": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Noch nichts abgespielt."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nothing played yet."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.relayWarning": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Über die Plex-Vermittlung. Stark gedrosselt — große Dateien laufen darüber kaum an."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Via the Plex relay. Heavily throttled — large files barely start over it."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.source": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Quelle (ohne Token)"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Source (token removed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.steps": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ablauf"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Timeline"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.title": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Diagnose"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Diagnostics"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.title.label": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Titel"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloads.active": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
@@ -2117,6 +2333,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.build": {
|
||||
"comment": "Zeitpunkt, zu dem der laufende Build entstanden ist",
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Build"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Build"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.connection": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
@@ -3180,227 +3414,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.build": {
|
||||
"comment": "Zeitpunkt, zu dem der laufende Build entstanden ist",
|
||||
"signIn.timedOut": {
|
||||
"comment": "Die Anmeldung wurde nicht innerhalb der Gültigkeit des Codes abgeschlossen",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Build"
|
||||
"value": "Die Anmeldung wurde nicht abgeschlossen. Versuch es noch einmal."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Build"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.title": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Diagnose"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Diagnostics"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.connection": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbindung"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.noServer": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Keine Serververbindung."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "No server connection."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.relayWarning": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Über die Plex-Vermittlung. Stark gedrosselt — große Dateien laufen darüber kaum an."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Via the Plex relay. Heavily throttled — large files barely start over it."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.lastAttempt": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Letzter Abspielversuch"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Last playback attempt"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.nothingYet": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Noch nichts abgespielt."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nothing played yet."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.title.label": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Titel"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.method": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Weg"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Method"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.connectionUsed": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbindung"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.source": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Quelle (ohne Token)"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Source (token removed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.steps": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ablauf"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Timeline"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.copy": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Bericht kopieren"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Copy report"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics.copyHint": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Enthält keinen Token. Kannst du unbesorgt weitergeben."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Contains no token. Safe to share."
|
||||
"value": "Sign-in wasn't completed. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,47 +150,77 @@ final class FlixySession {
|
||||
/// Öffnet die Anmeldeseite von Plex.
|
||||
///
|
||||
/// Der PIN steckt weiterhin darunter — er ist der einzige Weg, auf dem
|
||||
/// Plex einen Token herausgibt. Sichtbar ist er nicht mehr: Plex liefert
|
||||
/// seine eigene Anmeldemaske aus und schickt die App danach über
|
||||
/// `forwardUrl` zurück.
|
||||
/// Plex einen Token herausgibt. Sichtbar ist er nicht mehr.
|
||||
///
|
||||
/// Das Fenster wird geöffnet, aber **nicht abgewartet**: 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 dort stehen
|
||||
/// — der Token liegt längst bereit, aber die App wartete auf einen
|
||||
/// Rückruf, der nie kam. Stattdessen wird hier im Sekundentakt beim Server
|
||||
/// nachgefragt und nebenbei geprüft, ob das Fenster geschlossen wurde.
|
||||
func beginSignIn() async {
|
||||
phase = .authenticating
|
||||
|
||||
let pin: PlexPin
|
||||
do {
|
||||
let pin = try await authenticator.requestPin()
|
||||
pin = try await authenticator.requestPin()
|
||||
} catch {
|
||||
phase = .failed(message: message(for: error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = pin.authorizationURL(
|
||||
clientIdentifier: identity.clientIdentifier,
|
||||
product: Branding.plexProduct,
|
||||
forwardURL: Branding.authCallbackURL
|
||||
) else {
|
||||
phase = .failed(message: String(localized: "signIn.windowFailed"))
|
||||
guard let url = pin.authorizationURL(
|
||||
clientIdentifier: identity.clientIdentifier,
|
||||
product: Branding.plexProduct,
|
||||
forwardURL: Branding.authCallbackURL
|
||||
) else {
|
||||
phase = .failed(message: String(localized: "signIn.windowFailed"))
|
||||
return
|
||||
}
|
||||
|
||||
webAuthenticator.present(url: url, callbackScheme: Branding.urlScheme)
|
||||
|
||||
// Die Frist entspricht der Gültigkeit des Codes bei Plex.
|
||||
for _ in 0..<900 {
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
|
||||
if let token = try? await authenticator.token(for: pin) {
|
||||
webAuthenticator.dismiss()
|
||||
await connect(accountToken: token)
|
||||
return
|
||||
}
|
||||
|
||||
try await webAuthenticator.authenticate(
|
||||
url: url,
|
||||
callbackScheme: Branding.urlScheme
|
||||
)
|
||||
|
||||
// Das Fenster ist zu, jetzt liegt der Token bereit. Kurz
|
||||
// nachgefragt statt sofort erwartet: zwischen dem Rücksprung und
|
||||
// dem Eintrag bei Plex liegen manchmal ein, zwei Sekunden.
|
||||
let token = try await authenticator.waitForAuthorization(
|
||||
pin: pin,
|
||||
pollInterval: .milliseconds(500),
|
||||
timeout: .seconds(20)
|
||||
)
|
||||
await connect(accountToken: token)
|
||||
|
||||
} catch PlexWebAuthenticator.Failure.cancelled {
|
||||
// Fenster geschlossen. Kein Fehler — kommentarlos zurück.
|
||||
phase = .signedOut
|
||||
} catch let error as PlexWebAuthenticator.Failure {
|
||||
phase = .failed(message: webAuthenticationMessage(for: error))
|
||||
} catch {
|
||||
phase = .failed(message: message(for: error))
|
||||
// Fenster geschlossen, ohne dass ein Token da ist.
|
||||
if webAuthenticator.didReturn {
|
||||
switch webAuthenticator.outcome {
|
||||
case .cancelled, nil:
|
||||
// Abgebrochen oder zurückgesprungen, ohne dass der Token
|
||||
// schon bereitlag. Noch ein paar Sekunden nachfragen,
|
||||
// bevor aufgegeben wird — zwischen Rücksprung und Eintrag
|
||||
// bei Plex liegt manchmal eine Sekunde.
|
||||
if let token = try? await graceTokenLookup(pin: pin) {
|
||||
await connect(accountToken: token)
|
||||
return
|
||||
}
|
||||
phase = .signedOut
|
||||
case .presentationFailed(let description):
|
||||
phase = .failed(message: description)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
webAuthenticator.dismiss()
|
||||
phase = .failed(message: String(localized: "signIn.timedOut"))
|
||||
}
|
||||
|
||||
/// Fragt nach dem Schließen des Fensters noch ein paar Sekunden nach.
|
||||
private func graceTokenLookup(pin: PlexPin) async throws -> String? {
|
||||
for _ in 0..<5 {
|
||||
if let token = try? await authenticator.token(for: pin) { return token }
|
||||
try? await Task.sleep(for: .milliseconds(700))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelSignIn() {
|
||||
@@ -199,12 +229,6 @@ final class FlixySession {
|
||||
phase = .signedOut
|
||||
}
|
||||
|
||||
private func webAuthenticationMessage(for failure: PlexWebAuthenticator.Failure) -> String {
|
||||
switch failure {
|
||||
case .cancelled: String(localized: "signIn.cancelled")
|
||||
case .presentationFailed(let description): description
|
||||
}
|
||||
}
|
||||
|
||||
func signOut() {
|
||||
authorizationTask?.cancel()
|
||||
|
||||
@@ -21,42 +21,74 @@ final class PlexWebAuthenticator: NSObject {
|
||||
case presentationFailed(String)
|
||||
}
|
||||
|
||||
/// Führt die Anmeldung durch und kehrt zurück, sobald Plex die App
|
||||
/// zurückgeschickt hat.
|
||||
func authenticate(url: URL, callbackScheme: String) async throws(Failure) {
|
||||
do {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
|
||||
let session = ASWebAuthenticationSession(
|
||||
url: url,
|
||||
callbackURLScheme: callbackScheme
|
||||
) { _, error in
|
||||
if let error {
|
||||
continuation.resume(throwing: error)
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
/// 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?
|
||||
|
||||
session.presentationContextProvider = self
|
||||
// `false`, damit eine bestehende Plex-Anmeldung in Safari
|
||||
// mitgenutzt wird: wer dort schon angemeldet ist, braucht nur
|
||||
// zu bestätigen statt das Passwort erneut einzugeben.
|
||||
session.prefersEphemeralWebBrowserSession = false
|
||||
/// Wie das Fenster ausgegangen ist.
|
||||
///
|
||||
/// `nil`, solange es noch offen ist.
|
||||
private(set) var outcome: Failure?
|
||||
private(set) var didReturn = false
|
||||
|
||||
guard session.start() else {
|
||||
continuation.resume(throwing: Failure.presentationFailed(
|
||||
String(localized: "signIn.windowFailed")
|
||||
))
|
||||
return
|
||||
/// Ö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 VLCKit, 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
|
||||
}
|
||||
} catch let error as ASWebAuthenticationSessionError where error.code == .canceledLogin {
|
||||
throw .cancelled
|
||||
} catch let error as Failure {
|
||||
throw error
|
||||
} catch {
|
||||
throw .presentationFailed(error.localizedDescription)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user