refactor: share tracker/trakt auth and connect flow
This commit is contained in:
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Realtids-scrobbling",
|
||||
"scrobbleDescription": "Send afspil-, pause- og stop-begivenheder til Trakt under afspilning.",
|
||||
"watchedSync": "Synkroniser sét-status",
|
||||
"watchedSyncDescription": "Når du markerer ting som sét i Plezy, markeres de også på Trakt.",
|
||||
"connectFailed": "Kunne ikke forbinde til Trakt. Prøv igen."
|
||||
"watchedSyncDescription": "Når du markerer ting som sét i Plezy, markeres de også på Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Trackere",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Echtzeit-Scrobbling",
|
||||
"scrobbleDescription": "Sende Play-, Pause- und Stopp-Ereignisse während der Wiedergabe an Trakt.",
|
||||
"watchedSync": "Gesehen-Status synchronisieren",
|
||||
"watchedSyncDescription": "Wenn du Inhalte in Plezy als gesehen markierst, werden sie auch auf Trakt markiert.",
|
||||
"connectFailed": "Verbindung zu Trakt fehlgeschlagen. Versuche es erneut."
|
||||
"watchedSyncDescription": "Wenn du Inhalte in Plezy als gesehen markierst, werden sie auch auf Trakt markiert."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Tracker",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Real-time scrobbling",
|
||||
"scrobbleDescription": "Send play, pause, and stop events to Trakt during playback.",
|
||||
"watchedSync": "Sync watched status",
|
||||
"watchedSyncDescription": "When you mark items watched in Plezy, mark them on Trakt.",
|
||||
"connectFailed": "Couldn't connect to Trakt. Try again."
|
||||
"watchedSyncDescription": "When you mark items watched in Plezy, mark them on Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Trackers",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Scrobbling en tiempo real",
|
||||
"scrobbleDescription": "Enviar eventos de reproducción, pausa y parada a Trakt durante la reproducción.",
|
||||
"watchedSync": "Sincronizar estado de visto",
|
||||
"watchedSyncDescription": "Cuando marques elementos como vistos en Plezy, también se marcarán en Trakt.",
|
||||
"connectFailed": "No se pudo conectar a Trakt. Inténtalo de nuevo."
|
||||
"watchedSyncDescription": "Cuando marques elementos como vistos en Plezy, también se marcarán en Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Rastreadores",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Scrobbling en temps réel",
|
||||
"scrobbleDescription": "Envoyer les événements de lecture, pause et arrêt à Trakt pendant la lecture.",
|
||||
"watchedSync": "Synchroniser le statut « vu »",
|
||||
"watchedSyncDescription": "Lorsque vous marquez un élément comme vu dans Plezy, il l'est aussi sur Trakt.",
|
||||
"connectFailed": "Échec de la connexion à Trakt. Réessayez."
|
||||
"watchedSyncDescription": "Lorsque vous marquez un élément comme vu dans Plezy, il l'est aussi sur Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Trackers",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Scrobbling in tempo reale",
|
||||
"scrobbleDescription": "Invia eventi di riproduzione, pausa e arresto a Trakt durante la riproduzione.",
|
||||
"watchedSync": "Sincronizza stato visualizzato",
|
||||
"watchedSyncDescription": "Quando segni elementi come visti in Plezy, vengono segnati anche su Trakt.",
|
||||
"connectFailed": "Impossibile connettersi a Trakt. Riprova."
|
||||
"watchedSyncDescription": "Quando segni elementi come visti in Plezy, vengono segnati anche su Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Tracker",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "リアルタイムのスクロブル",
|
||||
"scrobbleDescription": "再生中に再生・一時停止・停止イベントを Trakt に送信します。",
|
||||
"watchedSync": "視聴済みステータスを同期",
|
||||
"watchedSyncDescription": "Plezy で項目を視聴済みにすると、Trakt でも視聴済みになります。",
|
||||
"connectFailed": "Trakt に接続できませんでした。もう一度お試しください。"
|
||||
"watchedSyncDescription": "Plezy で項目を視聴済みにすると、Trakt でも視聴済みになります。"
|
||||
},
|
||||
"trackers": {
|
||||
"title": "トラッカー",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "실시간 스크로블",
|
||||
"scrobbleDescription": "재생 중 재생, 일시정지, 정지 이벤트를 Trakt로 전송합니다.",
|
||||
"watchedSync": "시청 상태 동기화",
|
||||
"watchedSyncDescription": "Plezy에서 시청 완료로 표시한 항목이 Trakt에도 시청 완료로 표시됩니다.",
|
||||
"connectFailed": "Trakt에 연결할 수 없습니다. 다시 시도하세요."
|
||||
"watchedSyncDescription": "Plezy에서 시청 완료로 표시한 항목이 Trakt에도 시청 완료로 표시됩니다."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "트래커",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Sanntids-scrobbling",
|
||||
"scrobbleDescription": "Send avspillings-, pause- og stopphendelser til Trakt under avspilling.",
|
||||
"watchedSync": "Synkroniser sett-status",
|
||||
"watchedSyncDescription": "Når du markerer noe som sett i Plezy, markeres det også på Trakt.",
|
||||
"connectFailed": "Kunne ikke koble til Trakt. Prøv igjen."
|
||||
"watchedSyncDescription": "Når du markerer noe som sett i Plezy, markeres det også på Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Trackere",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Realtime scrobbling",
|
||||
"scrobbleDescription": "Verstuur play-, pauze- en stopgebeurtenissen tijdens afspelen naar Trakt.",
|
||||
"watchedSync": "Bekeken-status synchroniseren",
|
||||
"watchedSyncDescription": "Wanneer je items als bekeken markeert in Plezy, worden ze ook op Trakt gemarkeerd.",
|
||||
"connectFailed": "Kan niet verbinden met Trakt. Probeer opnieuw."
|
||||
"watchedSyncDescription": "Wanneer je items als bekeken markeert in Plezy, worden ze ook op Trakt gemarkeerd."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Trackers",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Scrobbling w czasie rzeczywistym",
|
||||
"scrobbleDescription": "Wysyłaj zdarzenia odtwarzania, pauzy i zatrzymania do Trakt podczas odtwarzania.",
|
||||
"watchedSync": "Synchronizuj status obejrzane",
|
||||
"watchedSyncDescription": "Gdy oznaczysz pozycje jako obejrzane w Plezy, zostaną też oznaczone w Trakt.",
|
||||
"connectFailed": "Nie udało się połączyć z Trakt. Spróbuj ponownie."
|
||||
"watchedSyncDescription": "Gdy oznaczysz pozycje jako obejrzane w Plezy, zostaną też oznaczone w Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Trackery",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Scrobbling em tempo real",
|
||||
"scrobbleDescription": "Envia eventos de reprodução, pausa e parada ao Trakt durante a exibição.",
|
||||
"watchedSync": "Sincronizar status de assistido",
|
||||
"watchedSyncDescription": "Ao marcar itens como assistidos no Plezy, eles também serão marcados no Trakt.",
|
||||
"connectFailed": "Não foi possível conectar ao Trakt. Tente novamente."
|
||||
"watchedSyncDescription": "Ao marcar itens como assistidos no Plezy, eles também serão marcados no Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Rastreadores",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Скробблинг в реальном времени",
|
||||
"scrobbleDescription": "Отправлять события воспроизведения, паузы и остановки в Trakt во время просмотра.",
|
||||
"watchedSync": "Синхронизация статуса просмотра",
|
||||
"watchedSyncDescription": "Когда вы отмечаете элементы как просмотренные в Plezy, они отмечаются и в Trakt.",
|
||||
"connectFailed": "Не удалось подключиться к Trakt. Попробуйте ещё раз."
|
||||
"watchedSyncDescription": "Когда вы отмечаете элементы как просмотренные в Plezy, они отмечаются и в Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Трекеры",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 15
|
||||
/// Strings: 14385 (959 per locale)
|
||||
/// Strings: 14370 (958 per locale)
|
||||
///
|
||||
/// Built on 2026-04-24 at 08:07 UTC
|
||||
/// Built on 2026-04-24 at 08:58 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktDa implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Send afspil-, pause- og stop-begivenheder til Trakt under afspilning.';
|
||||
@override String get watchedSync => 'Synkroniser sét-status';
|
||||
@override String get watchedSyncDescription => 'Når du markerer ting som sét i Plezy, markeres de også på Trakt.';
|
||||
@override String get connectFailed => 'Kunne ikke forbinde til Trakt. Prøv igen.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsDa {
|
||||
'trakt.scrobbleDescription' => 'Send afspil-, pause- og stop-begivenheder til Trakt under afspilning.',
|
||||
'trakt.watchedSync' => 'Synkroniser sét-status',
|
||||
'trakt.watchedSyncDescription' => 'Når du markerer ting som sét i Plezy, markeres de også på Trakt.',
|
||||
'trakt.connectFailed' => 'Kunne ikke forbinde til Trakt. Prøv igen.',
|
||||
'trackers.title' => 'Trackere',
|
||||
'trackers.hubSubtitle' => 'Hold din visningsfremgang synkroniseret med Trakt og andre tjenester.',
|
||||
'trackers.notConnected' => 'Ikke forbundet',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktDe implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Sende Play-, Pause- und Stopp-Ereignisse während der Wiedergabe an Trakt.';
|
||||
@override String get watchedSync => 'Gesehen-Status synchronisieren';
|
||||
@override String get watchedSyncDescription => 'Wenn du Inhalte in Plezy als gesehen markierst, werden sie auch auf Trakt markiert.';
|
||||
@override String get connectFailed => 'Verbindung zu Trakt fehlgeschlagen. Versuche es erneut.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsDe {
|
||||
'trakt.scrobbleDescription' => 'Sende Play-, Pause- und Stopp-Ereignisse während der Wiedergabe an Trakt.',
|
||||
'trakt.watchedSync' => 'Gesehen-Status synchronisieren',
|
||||
'trakt.watchedSyncDescription' => 'Wenn du Inhalte in Plezy als gesehen markierst, werden sie auch auf Trakt markiert.',
|
||||
'trakt.connectFailed' => 'Verbindung zu Trakt fehlgeschlagen. Versuche es erneut.',
|
||||
'trackers.title' => 'Tracker',
|
||||
'trackers.hubSubtitle' => 'Halte deinen Wiedergabefortschritt mit Trakt und anderen Diensten synchron.',
|
||||
'trackers.notConnected' => 'Nicht verbunden',
|
||||
|
||||
@@ -2948,9 +2948,6 @@ class TranslationsTraktEn {
|
||||
|
||||
/// en: 'When you mark items watched in Plezy, mark them on Trakt.'
|
||||
String get watchedSyncDescription => 'When you mark items watched in Plezy, mark them on Trakt.';
|
||||
|
||||
/// en: 'Couldn't connect to Trakt. Try again.'
|
||||
String get connectFailed => 'Couldn\'t connect to Trakt. Try again.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -4379,7 +4376,6 @@ extension on Translations {
|
||||
'trakt.scrobbleDescription' => 'Send play, pause, and stop events to Trakt during playback.',
|
||||
'trakt.watchedSync' => 'Sync watched status',
|
||||
'trakt.watchedSyncDescription' => 'When you mark items watched in Plezy, mark them on Trakt.',
|
||||
'trakt.connectFailed' => 'Couldn\'t connect to Trakt. Try again.',
|
||||
'trackers.title' => 'Trackers',
|
||||
'trackers.hubSubtitle' => 'Keep your watch progress in sync with Trakt and other services.',
|
||||
'trackers.notConnected' => 'Not connected',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktEs implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Enviar eventos de reproducción, pausa y parada a Trakt durante la reproducción.';
|
||||
@override String get watchedSync => 'Sincronizar estado de visto';
|
||||
@override String get watchedSyncDescription => 'Cuando marques elementos como vistos en Plezy, también se marcarán en Trakt.';
|
||||
@override String get connectFailed => 'No se pudo conectar a Trakt. Inténtalo de nuevo.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsEs {
|
||||
'trakt.scrobbleDescription' => 'Enviar eventos de reproducción, pausa y parada a Trakt durante la reproducción.',
|
||||
'trakt.watchedSync' => 'Sincronizar estado de visto',
|
||||
'trakt.watchedSyncDescription' => 'Cuando marques elementos como vistos en Plezy, también se marcarán en Trakt.',
|
||||
'trakt.connectFailed' => 'No se pudo conectar a Trakt. Inténtalo de nuevo.',
|
||||
'trackers.title' => 'Rastreadores',
|
||||
'trackers.hubSubtitle' => 'Mantén sincronizado tu progreso con Trakt y otros servicios.',
|
||||
'trackers.notConnected' => 'No conectado',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktFr implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Envoyer les événements de lecture, pause et arrêt à Trakt pendant la lecture.';
|
||||
@override String get watchedSync => 'Synchroniser le statut « vu »';
|
||||
@override String get watchedSyncDescription => 'Lorsque vous marquez un élément comme vu dans Plezy, il l\'est aussi sur Trakt.';
|
||||
@override String get connectFailed => 'Échec de la connexion à Trakt. Réessayez.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsFr {
|
||||
'trakt.scrobbleDescription' => 'Envoyer les événements de lecture, pause et arrêt à Trakt pendant la lecture.',
|
||||
'trakt.watchedSync' => 'Synchroniser le statut « vu »',
|
||||
'trakt.watchedSyncDescription' => 'Lorsque vous marquez un élément comme vu dans Plezy, il l\'est aussi sur Trakt.',
|
||||
'trakt.connectFailed' => 'Échec de la connexion à Trakt. Réessayez.',
|
||||
'trackers.title' => 'Trackers',
|
||||
'trackers.hubSubtitle' => 'Gardez votre progression synchronisée avec Trakt et d\'autres services.',
|
||||
'trackers.notConnected' => 'Non connecté',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktIt implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Invia eventi di riproduzione, pausa e arresto a Trakt durante la riproduzione.';
|
||||
@override String get watchedSync => 'Sincronizza stato visualizzato';
|
||||
@override String get watchedSyncDescription => 'Quando segni elementi come visti in Plezy, vengono segnati anche su Trakt.';
|
||||
@override String get connectFailed => 'Impossibile connettersi a Trakt. Riprova.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsIt {
|
||||
'trakt.scrobbleDescription' => 'Invia eventi di riproduzione, pausa e arresto a Trakt durante la riproduzione.',
|
||||
'trakt.watchedSync' => 'Sincronizza stato visualizzato',
|
||||
'trakt.watchedSyncDescription' => 'Quando segni elementi come visti in Plezy, vengono segnati anche su Trakt.',
|
||||
'trakt.connectFailed' => 'Impossibile connettersi a Trakt. Riprova.',
|
||||
'trackers.title' => 'Tracker',
|
||||
'trackers.hubSubtitle' => 'Mantieni i tuoi progressi sincronizzati con Trakt e altri servizi.',
|
||||
'trackers.notConnected' => 'Non connesso',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktJa implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => '再生中に再生・一時停止・停止イベントを Trakt に送信します。';
|
||||
@override String get watchedSync => '視聴済みステータスを同期';
|
||||
@override String get watchedSyncDescription => 'Plezy で項目を視聴済みにすると、Trakt でも視聴済みになります。';
|
||||
@override String get connectFailed => 'Trakt に接続できませんでした。もう一度お試しください。';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsJa {
|
||||
'trakt.scrobbleDescription' => '再生中に再生・一時停止・停止イベントを Trakt に送信します。',
|
||||
'trakt.watchedSync' => '視聴済みステータスを同期',
|
||||
'trakt.watchedSyncDescription' => 'Plezy で項目を視聴済みにすると、Trakt でも視聴済みになります。',
|
||||
'trakt.connectFailed' => 'Trakt に接続できませんでした。もう一度お試しください。',
|
||||
'trackers.title' => 'トラッカー',
|
||||
'trackers.hubSubtitle' => '視聴進捗を Trakt や他のサービスで同期します。',
|
||||
'trackers.notConnected' => '未接続',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktKo implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => '재생 중 재생, 일시정지, 정지 이벤트를 Trakt로 전송합니다.';
|
||||
@override String get watchedSync => '시청 상태 동기화';
|
||||
@override String get watchedSyncDescription => 'Plezy에서 시청 완료로 표시한 항목이 Trakt에도 시청 완료로 표시됩니다.';
|
||||
@override String get connectFailed => 'Trakt에 연결할 수 없습니다. 다시 시도하세요.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsKo {
|
||||
'trakt.scrobbleDescription' => '재생 중 재생, 일시정지, 정지 이벤트를 Trakt로 전송합니다.',
|
||||
'trakt.watchedSync' => '시청 상태 동기화',
|
||||
'trakt.watchedSyncDescription' => 'Plezy에서 시청 완료로 표시한 항목이 Trakt에도 시청 완료로 표시됩니다.',
|
||||
'trakt.connectFailed' => 'Trakt에 연결할 수 없습니다. 다시 시도하세요.',
|
||||
'trackers.title' => '트래커',
|
||||
'trackers.hubSubtitle' => '시청 진행률을 Trakt 및 기타 서비스와 동기화하세요.',
|
||||
'trackers.notConnected' => '연결되지 않음',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktNb implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Send avspillings-, pause- og stopphendelser til Trakt under avspilling.';
|
||||
@override String get watchedSync => 'Synkroniser sett-status';
|
||||
@override String get watchedSyncDescription => 'Når du markerer noe som sett i Plezy, markeres det også på Trakt.';
|
||||
@override String get connectFailed => 'Kunne ikke koble til Trakt. Prøv igjen.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsNb {
|
||||
'trakt.scrobbleDescription' => 'Send avspillings-, pause- og stopphendelser til Trakt under avspilling.',
|
||||
'trakt.watchedSync' => 'Synkroniser sett-status',
|
||||
'trakt.watchedSyncDescription' => 'Når du markerer noe som sett i Plezy, markeres det også på Trakt.',
|
||||
'trakt.connectFailed' => 'Kunne ikke koble til Trakt. Prøv igjen.',
|
||||
'trackers.title' => 'Trackere',
|
||||
'trackers.hubSubtitle' => 'Hold visningsfremdriften synkronisert med Trakt og andre tjenester.',
|
||||
'trackers.notConnected' => 'Ikke tilkoblet',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktNl implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Verstuur play-, pauze- en stopgebeurtenissen tijdens afspelen naar Trakt.';
|
||||
@override String get watchedSync => 'Bekeken-status synchroniseren';
|
||||
@override String get watchedSyncDescription => 'Wanneer je items als bekeken markeert in Plezy, worden ze ook op Trakt gemarkeerd.';
|
||||
@override String get connectFailed => 'Kan niet verbinden met Trakt. Probeer opnieuw.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsNl {
|
||||
'trakt.scrobbleDescription' => 'Verstuur play-, pauze- en stopgebeurtenissen tijdens afspelen naar Trakt.',
|
||||
'trakt.watchedSync' => 'Bekeken-status synchroniseren',
|
||||
'trakt.watchedSyncDescription' => 'Wanneer je items als bekeken markeert in Plezy, worden ze ook op Trakt gemarkeerd.',
|
||||
'trakt.connectFailed' => 'Kan niet verbinden met Trakt. Probeer opnieuw.',
|
||||
'trackers.title' => 'Trackers',
|
||||
'trackers.hubSubtitle' => 'Houd je kijkvoortgang gesynchroniseerd met Trakt en andere diensten.',
|
||||
'trackers.notConnected' => 'Niet verbonden',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktPl implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Wysyłaj zdarzenia odtwarzania, pauzy i zatrzymania do Trakt podczas odtwarzania.';
|
||||
@override String get watchedSync => 'Synchronizuj status obejrzane';
|
||||
@override String get watchedSyncDescription => 'Gdy oznaczysz pozycje jako obejrzane w Plezy, zostaną też oznaczone w Trakt.';
|
||||
@override String get connectFailed => 'Nie udało się połączyć z Trakt. Spróbuj ponownie.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsPl {
|
||||
'trakt.scrobbleDescription' => 'Wysyłaj zdarzenia odtwarzania, pauzy i zatrzymania do Trakt podczas odtwarzania.',
|
||||
'trakt.watchedSync' => 'Synchronizuj status obejrzane',
|
||||
'trakt.watchedSyncDescription' => 'Gdy oznaczysz pozycje jako obejrzane w Plezy, zostaną też oznaczone w Trakt.',
|
||||
'trakt.connectFailed' => 'Nie udało się połączyć z Trakt. Spróbuj ponownie.',
|
||||
'trackers.title' => 'Trackery',
|
||||
'trackers.hubSubtitle' => 'Synchronizuj postęp oglądania z Trakt i innymi usługami.',
|
||||
'trackers.notConnected' => 'Nie połączono',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktPt implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Envia eventos de reprodução, pausa e parada ao Trakt durante a exibição.';
|
||||
@override String get watchedSync => 'Sincronizar status de assistido';
|
||||
@override String get watchedSyncDescription => 'Ao marcar itens como assistidos no Plezy, eles também serão marcados no Trakt.';
|
||||
@override String get connectFailed => 'Não foi possível conectar ao Trakt. Tente novamente.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsPt {
|
||||
'trakt.scrobbleDescription' => 'Envia eventos de reprodução, pausa e parada ao Trakt durante a exibição.',
|
||||
'trakt.watchedSync' => 'Sincronizar status de assistido',
|
||||
'trakt.watchedSyncDescription' => 'Ao marcar itens como assistidos no Plezy, eles também serão marcados no Trakt.',
|
||||
'trakt.connectFailed' => 'Não foi possível conectar ao Trakt. Tente novamente.',
|
||||
'trackers.title' => 'Rastreadores',
|
||||
'trackers.hubSubtitle' => 'Mantenha seu progresso sincronizado com Trakt e outros serviços.',
|
||||
'trackers.notConnected' => 'Não conectado',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktRu implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Отправлять события воспроизведения, паузы и остановки в Trakt во время просмотра.';
|
||||
@override String get watchedSync => 'Синхронизация статуса просмотра';
|
||||
@override String get watchedSyncDescription => 'Когда вы отмечаете элементы как просмотренные в Plezy, они отмечаются и в Trakt.';
|
||||
@override String get connectFailed => 'Не удалось подключиться к Trakt. Попробуйте ещё раз.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsRu {
|
||||
'trakt.scrobbleDescription' => 'Отправлять события воспроизведения, паузы и остановки в Trakt во время просмотра.',
|
||||
'trakt.watchedSync' => 'Синхронизация статуса просмотра',
|
||||
'trakt.watchedSyncDescription' => 'Когда вы отмечаете элементы как просмотренные в Plezy, они отмечаются и в Trakt.',
|
||||
'trakt.connectFailed' => 'Не удалось подключиться к Trakt. Попробуйте ещё раз.',
|
||||
'trackers.title' => 'Трекеры',
|
||||
'trackers.hubSubtitle' => 'Синхронизируйте прогресс просмотра с Trakt и другими сервисами.',
|
||||
'trackers.notConnected' => 'Не подключено',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktSv implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => 'Skicka uppspelnings-, paus- och stopphändelser till Trakt under uppspelning.';
|
||||
@override String get watchedSync => 'Synkronisera tittad-status';
|
||||
@override String get watchedSyncDescription => 'När du markerar något som tittat i Plezy markeras det också på Trakt.';
|
||||
@override String get connectFailed => 'Kunde inte ansluta till Trakt. Försök igen.';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsSv {
|
||||
'trakt.scrobbleDescription' => 'Skicka uppspelnings-, paus- och stopphändelser till Trakt under uppspelning.',
|
||||
'trakt.watchedSync' => 'Synkronisera tittad-status',
|
||||
'trakt.watchedSyncDescription' => 'När du markerar något som tittat i Plezy markeras det också på Trakt.',
|
||||
'trakt.connectFailed' => 'Kunde inte ansluta till Trakt. Försök igen.',
|
||||
'trackers.title' => 'Spårare',
|
||||
'trackers.hubSubtitle' => 'Håll ditt tittarprogress synkroniserat med Trakt och andra tjänster.',
|
||||
'trackers.notConnected' => 'Ej ansluten',
|
||||
|
||||
@@ -1277,7 +1277,6 @@ class _TranslationsTraktZh implements TranslationsTraktEn {
|
||||
@override String get scrobbleDescription => '在播放时向 Trakt 发送播放、暂停和停止事件。';
|
||||
@override String get watchedSync => '同步已观看状态';
|
||||
@override String get watchedSyncDescription => '在 Plezy 中将内容标记为已观看时,也会在 Trakt 上标记为已观看。';
|
||||
@override String get connectFailed => '无法连接到 Trakt。请重试。';
|
||||
}
|
||||
|
||||
// Path: trackers
|
||||
@@ -2453,7 +2452,6 @@ extension on TranslationsZh {
|
||||
'trakt.scrobbleDescription' => '在播放时向 Trakt 发送播放、暂停和停止事件。',
|
||||
'trakt.watchedSync' => '同步已观看状态',
|
||||
'trakt.watchedSyncDescription' => '在 Plezy 中将内容标记为已观看时,也会在 Trakt 上标记为已观看。',
|
||||
'trakt.connectFailed' => '无法连接到 Trakt。请重试。',
|
||||
'trackers.title' => '追踪器',
|
||||
'trackers.hubSubtitle' => '让你的观看进度与 Trakt 及其他服务保持同步。',
|
||||
'trackers.notConnected' => '未连接',
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "Realtids-scrobbling",
|
||||
"scrobbleDescription": "Skicka uppspelnings-, paus- och stopphändelser till Trakt under uppspelning.",
|
||||
"watchedSync": "Synkronisera tittad-status",
|
||||
"watchedSyncDescription": "När du markerar något som tittat i Plezy markeras det också på Trakt.",
|
||||
"connectFailed": "Kunde inte ansluta till Trakt. Försök igen."
|
||||
"watchedSyncDescription": "När du markerar något som tittat i Plezy markeras det också på Trakt."
|
||||
},
|
||||
"trackers": {
|
||||
"title": "Spårare",
|
||||
|
||||
@@ -1016,8 +1016,7 @@
|
||||
"scrobble": "实时 Scrobble",
|
||||
"scrobbleDescription": "在播放时向 Trakt 发送播放、暂停和停止事件。",
|
||||
"watchedSync": "同步已观看状态",
|
||||
"watchedSyncDescription": "在 Plezy 中将内容标记为已观看时,也会在 Trakt 上标记为已观看。",
|
||||
"connectFailed": "无法连接到 Trakt。请重试。"
|
||||
"watchedSyncDescription": "在 Plezy 中将内容标记为已观看时,也会在 Trakt 上标记为已观看。"
|
||||
},
|
||||
"trackers": {
|
||||
"title": "追踪器",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/trackers/device_code.dart';
|
||||
@@ -20,6 +18,7 @@ import '../services/trackers/simkl/simkl_client.dart';
|
||||
import '../services/trackers/simkl/simkl_session.dart';
|
||||
import '../services/trackers/simkl/simkl_tracker.dart';
|
||||
import '../services/trackers/tracker_account_store.dart';
|
||||
import '../services/trackers/tracker_connect_runner.dart';
|
||||
import '../services/trackers/tracker_constants.dart';
|
||||
import '../services/trackers/tracker_coordinator.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
@@ -124,7 +123,10 @@ class TrackersProvider extends ChangeNotifier {
|
||||
Future<bool> connectSimkl({required void Function(DeviceCode code) onCodeReady}) => _runConnect<SimklSession>(
|
||||
service: TrackerService.simkl,
|
||||
alreadyConnected: isSimklConnected,
|
||||
authorize: () => _simklAuthorize(onCodeReady),
|
||||
authorize: () => _simklAuth.authorize(
|
||||
onCodeReady: onCodeReady,
|
||||
shouldCancel: () => _cancelRequested,
|
||||
),
|
||||
enrich: _enrichSimkl,
|
||||
store: simklAccountStore,
|
||||
assign: (s) {
|
||||
@@ -140,9 +142,7 @@ class TrackersProvider extends ChangeNotifier {
|
||||
|
||||
// ───── Connect machinery ─────
|
||||
|
||||
/// Shared connect shell: guard-if-busy, set in-flight flag, authorize,
|
||||
/// enrich, save, assign, clear flag. Errors are logged and surface as
|
||||
/// `false` so the UI can show a snack.
|
||||
/// Guard-if-busy, set in-flight flag, run the shared pipeline, clear flag.
|
||||
Future<bool> _runConnect<T>({
|
||||
required TrackerService service,
|
||||
required bool alreadyConnected,
|
||||
@@ -156,15 +156,13 @@ class TrackersProvider extends ChangeNotifier {
|
||||
_cancelRequested = false;
|
||||
notifyListeners();
|
||||
try {
|
||||
final raw = await authorize();
|
||||
if (raw == null) return false;
|
||||
final enriched = await enrich(raw);
|
||||
await store.save(_activeUserUuid, enriched);
|
||||
assign(enriched);
|
||||
return true;
|
||||
} catch (e) {
|
||||
appLogger.w('${service.name} connect failed', error: e);
|
||||
return false;
|
||||
return await runConnectPipeline<T>(
|
||||
logLabel: service.name,
|
||||
authorize: authorize,
|
||||
enrich: enrich,
|
||||
save: (s) => store.save(_activeUserUuid, s),
|
||||
assign: assign,
|
||||
);
|
||||
} finally {
|
||||
_connecting = null;
|
||||
notifyListeners();
|
||||
@@ -177,19 +175,6 @@ class TrackersProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<SimklSession?> _simklAuthorize(void Function(DeviceCode) onCodeReady) async {
|
||||
final code = await _simklAuth.createDeviceCode();
|
||||
onCodeReady(code);
|
||||
await for (final event in _simklAuth.pollDeviceCode(code, shouldCancel: () => _cancelRequested)) {
|
||||
if (event is DevicePollSuccess) {
|
||||
final token = event.tokenResponse['access_token'] as String;
|
||||
return SimklSession(accessToken: token, createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
}
|
||||
if (event is DevicePollDenied || event is DevicePollExpired) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<MalSession> _enrichMal(MalSession raw) async {
|
||||
try {
|
||||
final tmp = MalClient(raw, onSessionInvalidated: () {});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/trackers/device_code.dart';
|
||||
import '../services/trackers/tracker_connect_runner.dart';
|
||||
import '../services/trakt/trakt_account_store.dart';
|
||||
import '../services/trakt/trakt_auth_service.dart';
|
||||
import '../services/trakt/trakt_client.dart';
|
||||
@@ -47,49 +46,37 @@ class TraktAccountProvider extends ChangeNotifier {
|
||||
/// [onCodeReady] is invoked once with the user code + verification URL so
|
||||
/// the UI can render the dialog.
|
||||
Future<bool> connect({required void Function(DeviceCode code) onCodeReady}) async {
|
||||
if (_isConnecting) return false;
|
||||
if (_isConnecting || isConnected) return false;
|
||||
_isConnecting = true;
|
||||
_cancelRequested = false;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final code = await _auth.createDeviceCode();
|
||||
onCodeReady(code);
|
||||
|
||||
await for (final event in _auth.pollDeviceCode(code, shouldCancel: () => _cancelRequested)) {
|
||||
if (event is DevicePollSuccess) {
|
||||
await _completeConnect(event.tokenResponse);
|
||||
return true;
|
||||
}
|
||||
if (event is DevicePollDenied || event is DevicePollExpired) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
appLogger.w('Trakt connect failed', error: e);
|
||||
return false;
|
||||
return await runConnectPipeline<TraktSession>(
|
||||
logLabel: 'Trakt',
|
||||
authorize: () => _auth.authorize(
|
||||
onCodeReady: onCodeReady,
|
||||
shouldCancel: () => _cancelRequested,
|
||||
),
|
||||
enrich: _enrichUsername,
|
||||
save: (s) => _store.save(_activeUserUuid, s),
|
||||
assign: _setSessionAndRebind,
|
||||
);
|
||||
} finally {
|
||||
_isConnecting = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _completeConnect(Map<String, dynamic> tokenResponse) async {
|
||||
final raw = TraktSession.fromTokenResponse(tokenResponse);
|
||||
// Best-effort fetch of the username for display.
|
||||
TraktSession enriched = raw;
|
||||
Future<TraktSession> _enrichUsername(TraktSession raw) async {
|
||||
try {
|
||||
final tmpClient = TraktClient(raw, onSessionInvalidated: () {});
|
||||
final user = await tmpClient.getUserSettings();
|
||||
enriched = raw.copyWith(username: user.username);
|
||||
tmpClient.dispose();
|
||||
final tmp = TraktClient(raw, onSessionInvalidated: () {});
|
||||
final user = await tmp.getUserSettings();
|
||||
tmp.dispose();
|
||||
return raw.copyWith(username: user.username);
|
||||
} catch (e) {
|
||||
appLogger.d('Trakt: getUserSettings failed (non-fatal)', error: e);
|
||||
return raw;
|
||||
}
|
||||
|
||||
await _store.save(_activeUserUuid, enriched);
|
||||
_setSessionAndRebind(enriched);
|
||||
}
|
||||
|
||||
/// Revoke the access token and clear local state.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../focus/input_mode_tracker.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
|
||||
/// Shared "connect this tracker" launcher.
|
||||
///
|
||||
/// Handles the busy/already-connected guard, shows the service's code dialog
|
||||
/// once `connect` hands us a payload, auto-launches the browser on pointer
|
||||
/// platforms, closes the dialog when the flow resolves, and surfaces a failure
|
||||
/// snack. Service-specific pieces are supplied via [connect], [buildDialog],
|
||||
/// and [urlFor] so both `TrackersProvider`-backed and `TraktAccountProvider`-
|
||||
/// backed flows share one code path.
|
||||
Future<void> launchTrackerConnect<T>(
|
||||
BuildContext context, {
|
||||
required bool isBusyOrConnected,
|
||||
required String serviceName,
|
||||
required Future<bool> Function(void Function(T)) connect,
|
||||
required VoidCallback onCancel,
|
||||
required Widget Function(T payload, VoidCallback onCancel) buildDialog,
|
||||
required String Function(T payload) urlFor,
|
||||
}) async {
|
||||
if (isBusyOrConnected) return;
|
||||
|
||||
final autoLaunchBrowser = !InputModeTracker.isKeyboardMode(context);
|
||||
var dialogOpen = false;
|
||||
|
||||
final ok = await connect((payload) {
|
||||
if (!context.mounted) return;
|
||||
dialogOpen = true;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => buildDialog(payload, onCancel),
|
||||
).whenComplete(() => dialogOpen = false);
|
||||
if (autoLaunchBrowser) {
|
||||
unawaited(
|
||||
launchUrl(Uri.parse(urlFor(payload)), mode: LaunchMode.externalApplication).catchError((Object e) {
|
||||
appLogger.d('$serviceName: failed to auto-launch browser', error: e);
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
// Close the dialog iff we showed one and it's still up (not already closed by
|
||||
// the Cancel button). This is the ONLY site that dismisses the dialog —
|
||||
// popping here and having the dialog self-pop would pop the screen behind.
|
||||
if (dialogOpen) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
if (!ok) {
|
||||
showAppSnackBar(context, t.trackers.connectFailed(service: serviceName));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../focus/input_mode_tracker.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/trackers/device_code.dart';
|
||||
import '../../providers/trackers_provider.dart';
|
||||
@@ -15,71 +11,24 @@ import '../../services/trackers/oauth_proxy_client.dart';
|
||||
import '../../services/trackers/simkl/simkl_tracker.dart';
|
||||
import '../../services/trackers/tracker_constants.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/device_code_dialog.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/oauth_proxy_dialog.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
import 'tracker_connect_launcher.dart';
|
||||
import 'tracker_library_filter_screen.dart';
|
||||
|
||||
/// Shared dialog-driven connect flow used by all three trackers. Handles the
|
||||
/// guard, auto-launch-on-pointer, dialog lifecycle, and failure snackbar —
|
||||
/// service-specific pieces are delegated via [connect], [buildDialog], and
|
||||
/// [urlFor].
|
||||
Future<void> _startConnectionWithDialog<T>(
|
||||
BuildContext context, {
|
||||
required TrackerService service,
|
||||
required bool alreadyConnected,
|
||||
required String serviceName,
|
||||
required Future<bool> Function(void Function(T)) connect,
|
||||
required Widget Function(T payload, VoidCallback onCancel) buildDialog,
|
||||
required String Function(T payload) urlFor,
|
||||
}) async {
|
||||
final account = context.read<TrackersProvider>();
|
||||
if (account.isConnecting(service) || alreadyConnected) return;
|
||||
|
||||
final autoLaunchBrowser = !InputModeTracker.isKeyboardMode(context);
|
||||
var dialogOpen = false;
|
||||
|
||||
final ok = await connect((payload) {
|
||||
if (!context.mounted) return;
|
||||
dialogOpen = true;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => buildDialog(payload, account.cancelConnect),
|
||||
).whenComplete(() => dialogOpen = false);
|
||||
if (autoLaunchBrowser) {
|
||||
unawaited(
|
||||
launchUrl(Uri.parse(urlFor(payload)), mode: LaunchMode.externalApplication).catchError((Object e) {
|
||||
appLogger.d('${service.name}: failed to auto-launch browser', error: e);
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
if (dialogOpen) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
if (!ok) {
|
||||
showAppSnackBar(context, t.trackers.connectFailed(service: serviceName));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startMalConnection(BuildContext context) {
|
||||
final account = context.read<TrackersProvider>();
|
||||
final name = t.trackers.services.mal;
|
||||
return _startConnectionWithDialog<OAuthProxyStart>(
|
||||
return launchTrackerConnect<OAuthProxyStart>(
|
||||
context,
|
||||
service: TrackerService.mal,
|
||||
alreadyConnected: account.isMalConnected,
|
||||
isBusyOrConnected: account.isConnecting(TrackerService.mal) || account.isMalConnected,
|
||||
serviceName: name,
|
||||
connect: (cb) => account.connectMal(onCodeReady: cb),
|
||||
onCancel: account.cancelConnect,
|
||||
buildDialog: (p, cancel) => OAuthProxyDialog(start: p, serviceName: name, onCancel: cancel),
|
||||
urlFor: (p) => p.url,
|
||||
);
|
||||
@@ -88,12 +37,12 @@ Future<void> startMalConnection(BuildContext context) {
|
||||
Future<void> startAnilistConnection(BuildContext context) {
|
||||
final account = context.read<TrackersProvider>();
|
||||
final name = t.trackers.services.anilist;
|
||||
return _startConnectionWithDialog<OAuthProxyStart>(
|
||||
return launchTrackerConnect<OAuthProxyStart>(
|
||||
context,
|
||||
service: TrackerService.anilist,
|
||||
alreadyConnected: account.isAnilistConnected,
|
||||
isBusyOrConnected: account.isConnecting(TrackerService.anilist) || account.isAnilistConnected,
|
||||
serviceName: name,
|
||||
connect: (cb) => account.connectAnilist(onCodeReady: cb),
|
||||
onCancel: account.cancelConnect,
|
||||
buildDialog: (p, cancel) => OAuthProxyDialog(start: p, serviceName: name, onCancel: cancel),
|
||||
urlFor: (p) => p.url,
|
||||
);
|
||||
@@ -102,12 +51,12 @@ Future<void> startAnilistConnection(BuildContext context) {
|
||||
Future<void> startSimklConnection(BuildContext context) {
|
||||
final account = context.read<TrackersProvider>();
|
||||
final name = t.trackers.services.simkl;
|
||||
return _startConnectionWithDialog<DeviceCode>(
|
||||
return launchTrackerConnect<DeviceCode>(
|
||||
context,
|
||||
service: TrackerService.simkl,
|
||||
alreadyConnected: account.isSimklConnected,
|
||||
isBusyOrConnected: account.isConnecting(TrackerService.simkl) || account.isSimklConnected,
|
||||
serviceName: name,
|
||||
connect: (cb) => account.connectSimkl(onCodeReady: cb),
|
||||
onCancel: account.cancelConnect,
|
||||
buildDialog: (p, cancel) => DeviceCodeDialog(code: p, serviceName: name, onCancel: cancel),
|
||||
urlFor: (p) => p.verificationUrlComplete ?? p.verificationUrl,
|
||||
);
|
||||
|
||||
@@ -1,77 +1,34 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../focus/input_mode_tracker.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/trackers/device_code.dart';
|
||||
import '../../providers/trakt_account_provider.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../services/trackers/tracker_constants.dart';
|
||||
import '../../services/trakt/trakt_scrobble_service.dart';
|
||||
import '../../services/trakt/trakt_sync_service.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/device_code_dialog.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
import 'tracker_connect_launcher.dart';
|
||||
import 'tracker_library_filter_screen.dart';
|
||||
|
||||
/// Start the Trakt device-code OAuth flow from anywhere in the app.
|
||||
///
|
||||
/// Shows the code dialog + polls until the user completes authorization.
|
||||
/// On pointer-driven platforms (desktop + mobile when dpad/keyboard mode
|
||||
/// isn't active) this additionally auto-launches the browser at
|
||||
/// `trakt.tv/activate` with the code prefilled — the dialog still shows
|
||||
/// as a fallback/progress indicator.
|
||||
Future<void> startTraktConnection(BuildContext context) async {
|
||||
Future<void> startTraktConnection(BuildContext context) {
|
||||
final account = context.read<TraktAccountProvider>();
|
||||
if (account.isConnecting || account.isConnected) return;
|
||||
|
||||
final autoLaunchBrowser = !InputModeTracker.isKeyboardMode(context);
|
||||
var dialogOpen = false;
|
||||
|
||||
final ok = await account.connect(
|
||||
onCodeReady: (code) {
|
||||
if (!context.mounted) return;
|
||||
dialogOpen = true;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => DeviceCodeDialog(
|
||||
code: code,
|
||||
serviceName: t.trakt.title,
|
||||
onCancel: account.cancelConnect,
|
||||
),
|
||||
).whenComplete(() => dialogOpen = false);
|
||||
if (autoLaunchBrowser) {
|
||||
final url = code.verificationUrlComplete ?? code.verificationUrl;
|
||||
unawaited(
|
||||
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication).catchError((Object e) {
|
||||
appLogger.d('Trakt: failed to auto-launch browser', error: e);
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
final name = t.trakt.title;
|
||||
return launchTrackerConnect<DeviceCode>(
|
||||
context,
|
||||
isBusyOrConnected: account.isConnecting || account.isConnected,
|
||||
serviceName: name,
|
||||
connect: (cb) => account.connect(onCodeReady: cb),
|
||||
onCancel: account.cancelConnect,
|
||||
buildDialog: (code, cancel) => DeviceCodeDialog(code: code, serviceName: name, onCancel: cancel),
|
||||
urlFor: (code) => code.verificationUrlComplete ?? code.verificationUrl,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Close the dialog iff we showed one and it's still up (not already closed by
|
||||
// the Cancel button). This is the ONLY site that dismisses the dialog —
|
||||
// popping here and having the dialog self-pop would pop the screen behind.
|
||||
if (dialogOpen) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
|
||||
if (!ok && account.session == null) {
|
||||
showAppSnackBar(context, t.trakt.connectFailed);
|
||||
}
|
||||
}
|
||||
|
||||
class TraktSettingsScreen extends StatefulWidget {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../models/trackers/device_code.dart';
|
||||
import '../../utils/platform_http_client_stub.dart'
|
||||
if (dart.library.io) '../../utils/platform_http_client_io.dart'
|
||||
as platform;
|
||||
import 'device_code_poller.dart' as poller;
|
||||
|
||||
/// Base for RFC 8628 device-code auth services.
|
||||
///
|
||||
/// Owns the HTTP client lifecycle and the full "request code → invoke UI →
|
||||
/// poll → build session" dance. Subclasses only implement the three
|
||||
/// service-specific hooks: [createDeviceCode], [probe], [buildSession].
|
||||
abstract class DeviceCodeAuthServiceBase<T> {
|
||||
final http.Client httpClient;
|
||||
|
||||
DeviceCodeAuthServiceBase({http.Client? httpClient})
|
||||
: httpClient = httpClient ?? platform.createPlatformClient();
|
||||
|
||||
void dispose() => httpClient.close();
|
||||
|
||||
/// Service-specific device-code request.
|
||||
Future<DeviceCode> createDeviceCode();
|
||||
|
||||
/// One service-specific poll attempt — called at each interval tick.
|
||||
Future<DevicePollEvent> probe(DeviceCode code);
|
||||
|
||||
/// Build the session object from a successful token response.
|
||||
T buildSession(Map<String, dynamic> tokenResponse);
|
||||
|
||||
/// Drive the full flow. Invokes [onCodeReady] once with the code so the UI
|
||||
/// can render the dialog, then polls until the user authorizes, denies, or
|
||||
/// the code expires. Returns null on denied/expired/cancel.
|
||||
Future<T?> authorize({
|
||||
required void Function(DeviceCode code) onCodeReady,
|
||||
bool Function()? shouldCancel,
|
||||
}) async {
|
||||
final code = await createDeviceCode();
|
||||
onCodeReady(code);
|
||||
await for (final event in poller.pollDeviceCode(
|
||||
code,
|
||||
shouldCancel: shouldCancel,
|
||||
probe: () => probe(code),
|
||||
)) {
|
||||
if (event is DevicePollSuccess) return buildSession(event.tokenResponse);
|
||||
if (event is DevicePollDenied || event is DevicePollExpired) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceCodeAuthFlowException implements Exception {
|
||||
final String message;
|
||||
const DeviceCodeAuthFlowException(this.message);
|
||||
@override
|
||||
String toString() => 'DeviceCodeAuthFlowException: $message';
|
||||
}
|
||||
@@ -4,12 +4,10 @@ import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../models/trackers/device_code.dart';
|
||||
import '../../../utils/app_logger.dart';
|
||||
import '../../../utils/platform_http_client_stub.dart'
|
||||
if (dart.library.io) '../../../utils/platform_http_client_io.dart'
|
||||
as platform;
|
||||
import '../device_code_poller.dart' as poller;
|
||||
import '../device_code_auth_service.dart';
|
||||
import '../oauth_proxy_client.dart';
|
||||
import 'simkl_constants.dart';
|
||||
import 'simkl_session.dart';
|
||||
|
||||
/// Simkl OAuth PIN (device-code) flow.
|
||||
///
|
||||
@@ -17,13 +15,10 @@ import 'simkl_constants.dart';
|
||||
/// user enters at https://simkl.com/pin. After entry Simkl redirects the
|
||||
/// browser to the relay's static "signed in" page. The app polls
|
||||
/// `/oauth/pin/<user_code>?client_id=...` until `result == "OK"`.
|
||||
class SimklAuthService {
|
||||
final http.Client _http;
|
||||
|
||||
SimklAuthService({http.Client? httpClient}) : _http = httpClient ?? platform.createPlatformClient();
|
||||
|
||||
void dispose() => _http.close();
|
||||
class SimklAuthService extends DeviceCodeAuthServiceBase<SimklSession> {
|
||||
SimklAuthService({super.httpClient});
|
||||
|
||||
@override
|
||||
Future<DeviceCode> createDeviceCode() async {
|
||||
final uri = Uri.parse(SimklConstants.pinUrl).replace(
|
||||
queryParameters: {
|
||||
@@ -31,9 +26,9 @@ class SimklAuthService {
|
||||
'redirect': '${OAuthProxyClient.baseUrl}/auth/done',
|
||||
},
|
||||
);
|
||||
final res = await _http.get(uri, headers: SimklConstants.headers()).timeout(const Duration(seconds: 15));
|
||||
final res = await httpClient.get(uri, headers: SimklConstants.headers()).timeout(const Duration(seconds: 15));
|
||||
if (res.statusCode != 200) {
|
||||
throw SimklAuthFlowException('PIN request failed: HTTP ${res.statusCode}: ${res.body}');
|
||||
throw DeviceCodeAuthFlowException('Simkl PIN request failed: HTTP ${res.statusCode}: ${res.body}');
|
||||
}
|
||||
final body = json.decode(res.body) as Map<String, dynamic>;
|
||||
return DeviceCode(
|
||||
@@ -47,17 +42,14 @@ class SimklAuthService {
|
||||
);
|
||||
}
|
||||
|
||||
Stream<DevicePollEvent> pollDeviceCode(DeviceCode code, {bool Function()? shouldCancel}) {
|
||||
return poller.pollDeviceCode(code, shouldCancel: shouldCancel, probe: () => _probe(code));
|
||||
}
|
||||
|
||||
Future<DevicePollEvent> _probe(DeviceCode code) async {
|
||||
@override
|
||||
Future<DevicePollEvent> probe(DeviceCode code) async {
|
||||
final pollUri = Uri.parse(
|
||||
SimklConstants.pinPollUrl(code.userCode),
|
||||
).replace(queryParameters: {'client_id': SimklConstants.clientId});
|
||||
final http.Response res;
|
||||
try {
|
||||
res = await _http.get(pollUri, headers: SimklConstants.headers()).timeout(const Duration(seconds: 15));
|
||||
res = await httpClient.get(pollUri, headers: SimklConstants.headers()).timeout(const Duration(seconds: 15));
|
||||
} catch (e) {
|
||||
appLogger.d('Simkl device-code poll error (transient)', error: e);
|
||||
return const DevicePollPending();
|
||||
@@ -73,11 +65,7 @@ class SimklAuthService {
|
||||
}
|
||||
return const DevicePollPending();
|
||||
}
|
||||
}
|
||||
|
||||
class SimklAuthFlowException implements Exception {
|
||||
final String message;
|
||||
const SimklAuthFlowException(this.message);
|
||||
@override
|
||||
String toString() => 'SimklAuthFlowException: $message';
|
||||
SimklSession buildSession(Map<String, dynamic> tokenResponse) => SimklSession.fromTokenResponse(tokenResponse);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class SimklSession {
|
||||
createdAt: (json['created_at'] as num).toInt(),
|
||||
);
|
||||
|
||||
/// Build a session from Simkl's device-code `/oauth/pin/<code>` response.
|
||||
/// Simkl doesn't expose a creation timestamp so we stamp "now".
|
||||
factory SimklSession.fromTokenResponse(Map<String, dynamic> json) => SimklSession(
|
||||
accessToken: json['access_token'] as String,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
);
|
||||
|
||||
String encode() => json.encode(toJson());
|
||||
static SimklSession decode(String raw) => SimklSession.fromJson(json.decode(raw) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import '../../utils/app_logger.dart';
|
||||
|
||||
/// Shared "authorize → enrich → save → assign" pipeline.
|
||||
///
|
||||
/// Callers manage the in-flight flag + `notifyListeners` around this call;
|
||||
/// this helper only owns the inner steps so `TrackersProvider` and
|
||||
/// `TraktAccountProvider` can't drift. Returns `true` only on a fully applied
|
||||
/// session — null-from-authorize (cancel/denied/expired) and any exception
|
||||
/// both surface as `false`.
|
||||
Future<bool> runConnectPipeline<T>({
|
||||
required String logLabel,
|
||||
required Future<T?> Function() authorize,
|
||||
required Future<T> Function(T raw) enrich,
|
||||
required Future<void> Function(T enriched) save,
|
||||
required void Function(T enriched) assign,
|
||||
}) async {
|
||||
try {
|
||||
final raw = await authorize();
|
||||
if (raw == null) return false;
|
||||
final enriched = await enrich(raw);
|
||||
await save(enriched);
|
||||
assign(enriched);
|
||||
return true;
|
||||
} catch (e) {
|
||||
appLogger.w('$logLabel connect failed', error: e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,32 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../models/trackers/device_code.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/platform_http_client_stub.dart'
|
||||
if (dart.library.io) '../../utils/platform_http_client_io.dart'
|
||||
as platform;
|
||||
import '../trackers/device_code_poller.dart' as poller;
|
||||
import '../trackers/device_code_auth_service.dart';
|
||||
import 'trakt_constants.dart';
|
||||
import 'trakt_session.dart';
|
||||
|
||||
/// Trakt OAuth Device Authorization Grant flow (RFC 8628).
|
||||
///
|
||||
/// Mirrors the shape of `PlexAuthService.pollPinUntilClaimed`. The user enters
|
||||
/// a short code at `trakt.tv/activate` (in any browser); the app polls
|
||||
/// `/oauth/device/token` until the user completes the flow.
|
||||
class TraktAuthService {
|
||||
final http.Client _http;
|
||||
/// The user enters a short code at `trakt.tv/activate` (in any browser); the
|
||||
/// app polls `/oauth/device/token` until the user completes the flow.
|
||||
class TraktAuthService extends DeviceCodeAuthServiceBase<TraktSession> {
|
||||
TraktAuthService({super.httpClient});
|
||||
|
||||
TraktAuthService({http.Client? httpClient}) : _http = httpClient ?? platform.createPlatformClient();
|
||||
|
||||
void dispose() => _http.close();
|
||||
|
||||
/// Request a device code. The returned [DeviceCode.userCode] is what the
|
||||
/// user must enter at [DeviceCode.verificationUrl].
|
||||
@override
|
||||
Future<DeviceCode> createDeviceCode() async {
|
||||
final uri = Uri.parse(TraktConstants.deviceCodeUrl);
|
||||
final sw = Stopwatch()..start();
|
||||
final res = await _http
|
||||
final res = await httpClient
|
||||
.post(uri, headers: TraktConstants.headers(), body: json.encode({'client_id': TraktConstants.clientId}))
|
||||
.timeout(const Duration(seconds: 15));
|
||||
sw.stop();
|
||||
appLogger.d('Trakt POST ${uri.path} → ${res.statusCode} (${sw.elapsedMilliseconds}ms)');
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw TraktAuthFlowException('Device code request failed: HTTP ${res.statusCode}: ${res.body}');
|
||||
throw DeviceCodeAuthFlowException('Trakt device code request failed: HTTP ${res.statusCode}: ${res.body}');
|
||||
}
|
||||
|
||||
final body = json.decode(res.body) as Map<String, dynamic>;
|
||||
@@ -51,17 +42,12 @@ class TraktAuthService {
|
||||
);
|
||||
}
|
||||
|
||||
/// Poll the device-token endpoint until the user authorizes, denies, or the
|
||||
/// code expires. Pass [shouldCancel] to abort early.
|
||||
Stream<DevicePollEvent> pollDeviceCode(DeviceCode code, {bool Function()? shouldCancel}) {
|
||||
return poller.pollDeviceCode(code, shouldCancel: shouldCancel, probe: () => _probe(code));
|
||||
}
|
||||
|
||||
Future<DevicePollEvent> _probe(DeviceCode code) async {
|
||||
@override
|
||||
Future<DevicePollEvent> probe(DeviceCode code) async {
|
||||
final tokenUri = Uri.parse(TraktConstants.deviceTokenUrl);
|
||||
final http.Response res;
|
||||
try {
|
||||
res = await _http
|
||||
res = await httpClient
|
||||
.post(
|
||||
tokenUri,
|
||||
headers: TraktConstants.headers(),
|
||||
@@ -94,11 +80,7 @@ class TraktAuthService {
|
||||
return const DevicePollPending();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TraktAuthFlowException implements Exception {
|
||||
final String message;
|
||||
const TraktAuthFlowException(this.message);
|
||||
@override
|
||||
String toString() => 'TraktAuthFlowException: $message';
|
||||
TraktSession buildSession(Map<String, dynamic> tokenResponse) => TraktSession.fromTokenResponse(tokenResponse);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user