diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index cd6cb7b8..c770f7ec 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -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", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index c0375318..ac942d8c 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -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", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index dba37a47..98ef01eb 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -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", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index dcf9f5b7..ed94819a 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -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", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index c344abaa..2f8a2352 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -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", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 086d1a36..3448f4cf 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -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", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index eea05d73..5e138f85 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -1016,8 +1016,7 @@ "scrobble": "リアルタイムのスクロブル", "scrobbleDescription": "再生中に再生・一時停止・停止イベントを Trakt に送信します。", "watchedSync": "視聴済みステータスを同期", - "watchedSyncDescription": "Plezy で項目を視聴済みにすると、Trakt でも視聴済みになります。", - "connectFailed": "Trakt に接続できませんでした。もう一度お試しください。" + "watchedSyncDescription": "Plezy で項目を視聴済みにすると、Trakt でも視聴済みになります。" }, "trackers": { "title": "トラッカー", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 8f3596ca..a4b95491 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -1016,8 +1016,7 @@ "scrobble": "실시간 스크로블", "scrobbleDescription": "재생 중 재생, 일시정지, 정지 이벤트를 Trakt로 전송합니다.", "watchedSync": "시청 상태 동기화", - "watchedSyncDescription": "Plezy에서 시청 완료로 표시한 항목이 Trakt에도 시청 완료로 표시됩니다.", - "connectFailed": "Trakt에 연결할 수 없습니다. 다시 시도하세요." + "watchedSyncDescription": "Plezy에서 시청 완료로 표시한 항목이 Trakt에도 시청 완료로 표시됩니다." }, "trackers": { "title": "트래커", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index bd1bdccd..f82ca017 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -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", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index a88a5056..b29867e8 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -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", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index f0b20f1b..e324e4e3 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -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", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index b082b863..0b2ac1c6 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -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", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index ed87b332..14803889 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -1016,8 +1016,7 @@ "scrobble": "Скробблинг в реальном времени", "scrobbleDescription": "Отправлять события воспроизведения, паузы и остановки в Trakt во время просмотра.", "watchedSync": "Синхронизация статуса просмотра", - "watchedSyncDescription": "Когда вы отмечаете элементы как просмотренные в Plezy, они отмечаются и в Trakt.", - "connectFailed": "Не удалось подключиться к Trakt. Попробуйте ещё раз." + "watchedSyncDescription": "Когда вы отмечаете элементы как просмотренные в Plezy, они отмечаются и в Trakt." }, "trackers": { "title": "Трекеры", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index d9041a62..f7c446d4 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -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 diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index adf93619..0bc3fe20 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -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', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 071e76bd..bd66455a 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -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', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index a498e3f0..259352df 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -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', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 40f02070..d98a6529 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -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', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index c83f7921..f337fa58 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -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é', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 93f755eb..47c52a67 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -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', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index a47ce87b..4c2679d1 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -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' => '未接続', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 8432bb63..3f66f026 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -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' => '연결되지 않음', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index fe5d7d92..70091111 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -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', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index dda25320..bedeccac 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -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', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index b98b0189..fca35718 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -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', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index b21d0eab..05351192 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -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', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 1a459175..f5dd0908 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -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' => 'Не подключено', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index d1d437f6..cd7af186 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -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', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index d51b4cc8..a0e570a7 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -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' => '未连接', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 770b5e6c..931b022b 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -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", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 54e46d12..854a799c 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -1016,8 +1016,7 @@ "scrobble": "实时 Scrobble", "scrobbleDescription": "在播放时向 Trakt 发送播放、暂停和停止事件。", "watchedSync": "同步已观看状态", - "watchedSyncDescription": "在 Plezy 中将内容标记为已观看时,也会在 Trakt 上标记为已观看。", - "connectFailed": "无法连接到 Trakt。请重试。" + "watchedSyncDescription": "在 Plezy 中将内容标记为已观看时,也会在 Trakt 上标记为已观看。" }, "trackers": { "title": "追踪器", diff --git a/lib/providers/trackers_provider.dart b/lib/providers/trackers_provider.dart index a064a204..0c4a4dca 100644 --- a/lib/providers/trackers_provider.dart +++ b/lib/providers/trackers_provider.dart @@ -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 connectSimkl({required void Function(DeviceCode code) onCodeReady}) => _runConnect( 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 _runConnect({ 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( + 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 _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 _enrichMal(MalSession raw) async { try { final tmp = MalClient(raw, onSessionInvalidated: () {}); diff --git a/lib/providers/trakt_account_provider.dart b/lib/providers/trakt_account_provider.dart index df7a5d0a..1367df65 100644 --- a/lib/providers/trakt_account_provider.dart +++ b/lib/providers/trakt_account_provider.dart @@ -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 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( + logLabel: 'Trakt', + authorize: () => _auth.authorize( + onCodeReady: onCodeReady, + shouldCancel: () => _cancelRequested, + ), + enrich: _enrichUsername, + save: (s) => _store.save(_activeUserUuid, s), + assign: _setSessionAndRebind, + ); } finally { _isConnecting = false; notifyListeners(); } } - Future _completeConnect(Map tokenResponse) async { - final raw = TraktSession.fromTokenResponse(tokenResponse); - // Best-effort fetch of the username for display. - TraktSession enriched = raw; + Future _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. diff --git a/lib/screens/settings/tracker_connect_launcher.dart b/lib/screens/settings/tracker_connect_launcher.dart new file mode 100644 index 00000000..703ae1aa --- /dev/null +++ b/lib/screens/settings/tracker_connect_launcher.dart @@ -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 launchTrackerConnect( + BuildContext context, { + required bool isBusyOrConnected, + required String serviceName, + required Future 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( + 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)); + } +} diff --git a/lib/screens/settings/tracker_settings_screen.dart b/lib/screens/settings/tracker_settings_screen.dart index 46e35b63..052b30be 100644 --- a/lib/screens/settings/tracker_settings_screen.dart +++ b/lib/screens/settings/tracker_settings_screen.dart @@ -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 _startConnectionWithDialog( - BuildContext context, { - required TrackerService service, - required bool alreadyConnected, - required String serviceName, - required Future Function(void Function(T)) connect, - required Widget Function(T payload, VoidCallback onCancel) buildDialog, - required String Function(T payload) urlFor, -}) async { - final account = context.read(); - 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( - 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 startMalConnection(BuildContext context) { final account = context.read(); final name = t.trackers.services.mal; - return _startConnectionWithDialog( + return launchTrackerConnect( 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 startMalConnection(BuildContext context) { Future startAnilistConnection(BuildContext context) { final account = context.read(); final name = t.trackers.services.anilist; - return _startConnectionWithDialog( + return launchTrackerConnect( 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 startAnilistConnection(BuildContext context) { Future startSimklConnection(BuildContext context) { final account = context.read(); final name = t.trackers.services.simkl; - return _startConnectionWithDialog( + return launchTrackerConnect( 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, ); diff --git a/lib/screens/settings/trakt_settings_screen.dart b/lib/screens/settings/trakt_settings_screen.dart index 4d231b95..4d64f9e9 100644 --- a/lib/screens/settings/trakt_settings_screen.dart +++ b/lib/screens/settings/trakt_settings_screen.dart @@ -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 startTraktConnection(BuildContext context) async { +Future startTraktConnection(BuildContext context) { final account = context.read(); - 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( - 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( + 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 { diff --git a/lib/services/trackers/device_code_auth_service.dart b/lib/services/trackers/device_code_auth_service.dart new file mode 100644 index 00000000..d7803208 --- /dev/null +++ b/lib/services/trackers/device_code_auth_service.dart @@ -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 { + final http.Client httpClient; + + DeviceCodeAuthServiceBase({http.Client? httpClient}) + : httpClient = httpClient ?? platform.createPlatformClient(); + + void dispose() => httpClient.close(); + + /// Service-specific device-code request. + Future createDeviceCode(); + + /// One service-specific poll attempt — called at each interval tick. + Future probe(DeviceCode code); + + /// Build the session object from a successful token response. + T buildSession(Map 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 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'; +} diff --git a/lib/services/trackers/simkl/simkl_auth_service.dart b/lib/services/trackers/simkl/simkl_auth_service.dart index da7c1253..6439e466 100644 --- a/lib/services/trackers/simkl/simkl_auth_service.dart +++ b/lib/services/trackers/simkl/simkl_auth_service.dart @@ -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/?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 { + SimklAuthService({super.httpClient}); + @override Future 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; return DeviceCode( @@ -47,17 +42,14 @@ class SimklAuthService { ); } - Stream pollDeviceCode(DeviceCode code, {bool Function()? shouldCancel}) { - return poller.pollDeviceCode(code, shouldCancel: shouldCancel, probe: () => _probe(code)); - } - - Future _probe(DeviceCode code) async { + @override + Future 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 tokenResponse) => SimklSession.fromTokenResponse(tokenResponse); } diff --git a/lib/services/trackers/simkl/simkl_session.dart b/lib/services/trackers/simkl/simkl_session.dart index fbf424ca..7035f226 100644 --- a/lib/services/trackers/simkl/simkl_session.dart +++ b/lib/services/trackers/simkl/simkl_session.dart @@ -29,6 +29,13 @@ class SimklSession { createdAt: (json['created_at'] as num).toInt(), ); + /// Build a session from Simkl's device-code `/oauth/pin/` response. + /// Simkl doesn't expose a creation timestamp so we stamp "now". + factory SimklSession.fromTokenResponse(Map 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); } diff --git a/lib/services/trackers/tracker_connect_runner.dart b/lib/services/trackers/tracker_connect_runner.dart new file mode 100644 index 00000000..d9684061 --- /dev/null +++ b/lib/services/trackers/tracker_connect_runner.dart @@ -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 runConnectPipeline({ + required String logLabel, + required Future Function() authorize, + required Future Function(T raw) enrich, + required Future 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; + } +} diff --git a/lib/services/trakt/trakt_auth_service.dart b/lib/services/trakt/trakt_auth_service.dart index 2e96a418..e9dd3d61 100644 --- a/lib/services/trakt/trakt_auth_service.dart +++ b/lib/services/trakt/trakt_auth_service.dart @@ -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 { + 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 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; @@ -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 pollDeviceCode(DeviceCode code, {bool Function()? shouldCancel}) { - return poller.pollDeviceCode(code, shouldCancel: shouldCancel, probe: () => _probe(code)); - } - - Future _probe(DeviceCode code) async { + @override + Future 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 tokenResponse) => TraktSession.fromTokenResponse(tokenResponse); }