fix: watch together bugs, TV focus issues, system locale default
- Fix guest controls locked on join (use reactive context.select) - Fix Google TV back button when controls hidden (focus recovery + global fallback) - Fix profile switch screen focus lost on TV (explicit focus request) - Add 15s host reconnect grace period with reconnecting overlay - Add participant join/leave notification toasts - Default to system language on first launch via findDeviceLocale()
This commit is contained in:
@@ -644,7 +644,10 @@
|
|||||||
"failedToCreate": "Sitzung konnte nicht erstellt werden",
|
"failedToCreate": "Sitzung konnte nicht erstellt werden",
|
||||||
"failedToJoin": "Sitzung konnte nicht beigetreten werden",
|
"failedToJoin": "Sitzung konnte nicht beigetreten werden",
|
||||||
"sessionCodeCopied": "Sitzungscode in Zwischenablage kopiert",
|
"sessionCodeCopied": "Sitzungscode in Zwischenablage kopiert",
|
||||||
"relayUnreachable": "Der Relay-Server ist nicht erreichbar. Möglicherweise blockiert dein Internetanbieter die Verbindung. Du kannst es trotzdem versuchen, aber Watch Together funktioniert eventuell nicht."
|
"relayUnreachable": "Der Relay-Server ist nicht erreichbar. Möglicherweise blockiert dein Internetanbieter die Verbindung. Du kannst es trotzdem versuchen, aber Watch Together funktioniert eventuell nicht.",
|
||||||
|
"reconnectingToHost": "Verbindung zum Host wird wiederhergestellt...",
|
||||||
|
"participantJoined": "${name} ist beigetreten",
|
||||||
|
"participantLeft": "${name} hat die Sitzung verlassen"
|
||||||
},
|
},
|
||||||
"shaders": {
|
"shaders": {
|
||||||
"title": "Shader",
|
"title": "Shader",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"failedToCreate": "Failed to create session",
|
"failedToCreate": "Failed to create session",
|
||||||
"failedToJoin": "Failed to join session",
|
"failedToJoin": "Failed to join session",
|
||||||
"sessionCodeCopied": "Session code copied to clipboard",
|
"sessionCodeCopied": "Session code copied to clipboard",
|
||||||
"relayUnreachable": "The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work."
|
"relayUnreachable": "The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.",
|
||||||
|
"reconnectingToHost": "Reconnecting to host...",
|
||||||
|
"participantJoined": "${name} joined",
|
||||||
|
"participantLeft": "${name} left"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"title": "Downloads",
|
"title": "Downloads",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"failedToCreate": "Error al crear la sesión",
|
"failedToCreate": "Error al crear la sesión",
|
||||||
"failedToJoin": "Error al unirse a la sesión",
|
"failedToJoin": "Error al unirse a la sesión",
|
||||||
"sessionCodeCopied": "Código de sesión copiado al portapapeles",
|
"sessionCodeCopied": "Código de sesión copiado al portapapeles",
|
||||||
"relayUnreachable": "El servidor de retransmisión no está disponible. Esto puede deberse a que tu proveedor de internet bloquea la conexión. Puedes intentarlo de todos modos, pero Watch Together podría no funcionar."
|
"relayUnreachable": "El servidor de retransmisión no está disponible. Esto puede deberse a que tu proveedor de internet bloquea la conexión. Puedes intentarlo de todos modos, pero Watch Together podría no funcionar.",
|
||||||
|
"reconnectingToHost": "Reconectando con el anfitrión...",
|
||||||
|
"participantJoined": "${name} se unió",
|
||||||
|
"participantLeft": "${name} se fue"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"title": "Descargas",
|
"title": "Descargas",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"failedToCreate": "Échec de la création de la session",
|
"failedToCreate": "Échec de la création de la session",
|
||||||
"failedToJoin": "Échec de la connexion à la session",
|
"failedToJoin": "Échec de la connexion à la session",
|
||||||
"sessionCodeCopied": "Code de session copié dans le presse-papiers",
|
"sessionCodeCopied": "Code de session copié dans le presse-papiers",
|
||||||
"relayUnreachable": "Le serveur relais est inaccessible. Cela peut être dû au blocage de la connexion par votre fournisseur d'accès. Vous pouvez quand même essayer, mais Watch Together pourrait ne pas fonctionner."
|
"relayUnreachable": "Le serveur relais est inaccessible. Cela peut être dû au blocage de la connexion par votre fournisseur d'accès. Vous pouvez quand même essayer, mais Watch Together pourrait ne pas fonctionner.",
|
||||||
|
"reconnectingToHost": "Reconnexion à l'hôte...",
|
||||||
|
"participantJoined": "${name} a rejoint",
|
||||||
|
"participantLeft": "${name} est parti"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"title": "Téléchargements",
|
"title": "Téléchargements",
|
||||||
|
|||||||
@@ -644,7 +644,10 @@
|
|||||||
"failedToCreate": "Impossibile creare la sessione",
|
"failedToCreate": "Impossibile creare la sessione",
|
||||||
"failedToJoin": "Impossibile unirsi alla sessione",
|
"failedToJoin": "Impossibile unirsi alla sessione",
|
||||||
"sessionCodeCopied": "Codice sessione copiato negli appunti",
|
"sessionCodeCopied": "Codice sessione copiato negli appunti",
|
||||||
"relayUnreachable": "Il server di inoltro non è raggiungibile. Questo potrebbe essere causato dal blocco della connessione da parte del tuo provider. Puoi comunque provare, ma Watch Together potrebbe non funzionare."
|
"relayUnreachable": "Il server di inoltro non è raggiungibile. Questo potrebbe essere causato dal blocco della connessione da parte del tuo provider. Puoi comunque provare, ma Watch Together potrebbe non funzionare.",
|
||||||
|
"reconnectingToHost": "Riconnessione all'host...",
|
||||||
|
"participantJoined": "${name} si è unito",
|
||||||
|
"participantLeft": "${name} se ne è andato"
|
||||||
},
|
},
|
||||||
"shaders": {
|
"shaders": {
|
||||||
"title": "Shader",
|
"title": "Shader",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"failedToCreate": "세션 생성 실패",
|
"failedToCreate": "세션 생성 실패",
|
||||||
"failedToJoin": "세션 참여 실패",
|
"failedToJoin": "세션 참여 실패",
|
||||||
"sessionCodeCopied": "세션 코드가 클립보드에 복사되었습니다",
|
"sessionCodeCopied": "세션 코드가 클립보드에 복사되었습니다",
|
||||||
"relayUnreachable": "릴레이 서버에 연결할 수 없습니다. 인터넷 제공업체가 연결을 차단하고 있을 수 있습니다. 시도해 볼 수 있지만 함께 보기가 작동하지 않을 수 있습니다."
|
"relayUnreachable": "릴레이 서버에 연결할 수 없습니다. 인터넷 제공업체가 연결을 차단하고 있을 수 있습니다. 시도해 볼 수 있지만 함께 보기가 작동하지 않을 수 있습니다.",
|
||||||
|
"reconnectingToHost": "호스트에 재연결 중...",
|
||||||
|
"participantJoined": "${name}님이 참여했습니다",
|
||||||
|
"participantLeft": "${name}님이 나갔습니다"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"title": "다운로드",
|
"title": "다운로드",
|
||||||
|
|||||||
@@ -644,7 +644,10 @@
|
|||||||
"failedToCreate": "Sessie maken mislukt",
|
"failedToCreate": "Sessie maken mislukt",
|
||||||
"failedToJoin": "Sessie deelnemen mislukt",
|
"failedToJoin": "Sessie deelnemen mislukt",
|
||||||
"sessionCodeCopied": "Sessiecode gekopieerd naar klembord",
|
"sessionCodeCopied": "Sessiecode gekopieerd naar klembord",
|
||||||
"relayUnreachable": "De relayserver is niet bereikbaar. Dit kan worden veroorzaakt doordat je internetprovider de verbinding blokkeert. Je kunt het toch proberen, maar Watch Together werkt mogelijk niet."
|
"relayUnreachable": "De relayserver is niet bereikbaar. Dit kan worden veroorzaakt doordat je internetprovider de verbinding blokkeert. Je kunt het toch proberen, maar Watch Together werkt mogelijk niet.",
|
||||||
|
"reconnectingToHost": "Opnieuw verbinden met host...",
|
||||||
|
"participantJoined": "${name} is toegetreden",
|
||||||
|
"participantLeft": "${name} heeft de sessie verlaten"
|
||||||
},
|
},
|
||||||
"shaders": {
|
"shaders": {
|
||||||
"title": "Shaders",
|
"title": "Shaders",
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 9
|
/// Locales: 9
|
||||||
/// Strings: 6057 (673 per locale)
|
/// Strings: 6084 (676 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-02-10 at 21:37 UTC
|
/// Built on 2026-02-10 at 21:45 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -905,6 +905,9 @@ class _TranslationsWatchTogetherDe implements TranslationsWatchTogetherEn {
|
|||||||
@override String get failedToJoin => 'Sitzung konnte nicht beigetreten werden';
|
@override String get failedToJoin => 'Sitzung konnte nicht beigetreten werden';
|
||||||
@override String get sessionCodeCopied => 'Sitzungscode in Zwischenablage kopiert';
|
@override String get sessionCodeCopied => 'Sitzungscode in Zwischenablage kopiert';
|
||||||
@override String get relayUnreachable => 'Der Relay-Server ist nicht erreichbar. Möglicherweise blockiert dein Internetanbieter die Verbindung. Du kannst es trotzdem versuchen, aber Watch Together funktioniert eventuell nicht.';
|
@override String get relayUnreachable => 'Der Relay-Server ist nicht erreichbar. Möglicherweise blockiert dein Internetanbieter die Verbindung. Du kannst es trotzdem versuchen, aber Watch Together funktioniert eventuell nicht.';
|
||||||
|
@override String get reconnectingToHost => 'Verbindung zum Host wird wiederhergestellt...';
|
||||||
|
@override String participantJoined({required Object name}) => '${name} ist beigetreten';
|
||||||
|
@override String participantLeft({required Object name}) => '${name} hat die Sitzung verlassen';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: shaders
|
// Path: shaders
|
||||||
@@ -1717,6 +1720,9 @@ extension on TranslationsDe {
|
|||||||
'watchTogether.failedToJoin' => 'Sitzung konnte nicht beigetreten werden',
|
'watchTogether.failedToJoin' => 'Sitzung konnte nicht beigetreten werden',
|
||||||
'watchTogether.sessionCodeCopied' => 'Sitzungscode in Zwischenablage kopiert',
|
'watchTogether.sessionCodeCopied' => 'Sitzungscode in Zwischenablage kopiert',
|
||||||
'watchTogether.relayUnreachable' => 'Der Relay-Server ist nicht erreichbar. Möglicherweise blockiert dein Internetanbieter die Verbindung. Du kannst es trotzdem versuchen, aber Watch Together funktioniert eventuell nicht.',
|
'watchTogether.relayUnreachable' => 'Der Relay-Server ist nicht erreichbar. Möglicherweise blockiert dein Internetanbieter die Verbindung. Du kannst es trotzdem versuchen, aber Watch Together funktioniert eventuell nicht.',
|
||||||
|
'watchTogether.reconnectingToHost' => 'Verbindung zum Host wird wiederhergestellt...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name} ist beigetreten',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name} hat die Sitzung verlassen',
|
||||||
'shaders.title' => 'Shader',
|
'shaders.title' => 'Shader',
|
||||||
'shaders.noShaderDescription' => 'Keine Videoverbesserung',
|
'shaders.noShaderDescription' => 'Keine Videoverbesserung',
|
||||||
'shaders.nvscalerDescription' => 'NVIDIA-Bildskalierung für schärferes Video',
|
'shaders.nvscalerDescription' => 'NVIDIA-Bildskalierung für schärferes Video',
|
||||||
|
|||||||
@@ -1930,6 +1930,15 @@ class TranslationsWatchTogetherEn {
|
|||||||
|
|
||||||
/// en: 'The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.'
|
/// en: 'The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.'
|
||||||
String get relayUnreachable => 'The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.';
|
String get relayUnreachable => 'The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.';
|
||||||
|
|
||||||
|
/// en: 'Reconnecting to host...'
|
||||||
|
String get reconnectingToHost => 'Reconnecting to host...';
|
||||||
|
|
||||||
|
/// en: '${name} joined'
|
||||||
|
String participantJoined({required Object name}) => '${name} joined';
|
||||||
|
|
||||||
|
/// en: '${name} left'
|
||||||
|
String participantLeft({required Object name}) => '${name} left';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: downloads
|
// Path: downloads
|
||||||
@@ -3052,6 +3061,9 @@ extension on Translations {
|
|||||||
'watchTogether.failedToJoin' => 'Failed to join session',
|
'watchTogether.failedToJoin' => 'Failed to join session',
|
||||||
'watchTogether.sessionCodeCopied' => 'Session code copied to clipboard',
|
'watchTogether.sessionCodeCopied' => 'Session code copied to clipboard',
|
||||||
'watchTogether.relayUnreachable' => 'The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.',
|
'watchTogether.relayUnreachable' => 'The relay server is unreachable. This may be caused by your ISP blocking the connection. You can still try, but Watch Together may not work.',
|
||||||
|
'watchTogether.reconnectingToHost' => 'Reconnecting to host...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name} joined',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name} left',
|
||||||
'downloads.title' => 'Downloads',
|
'downloads.title' => 'Downloads',
|
||||||
'downloads.manage' => 'Manage',
|
'downloads.manage' => 'Manage',
|
||||||
'downloads.tvShows' => 'TV Shows',
|
'downloads.tvShows' => 'TV Shows',
|
||||||
|
|||||||
@@ -878,6 +878,9 @@ class _TranslationsWatchTogetherEs implements TranslationsWatchTogetherEn {
|
|||||||
@override String get failedToJoin => 'Error al unirse a la sesión';
|
@override String get failedToJoin => 'Error al unirse a la sesión';
|
||||||
@override String get sessionCodeCopied => 'Código de sesión copiado al portapapeles';
|
@override String get sessionCodeCopied => 'Código de sesión copiado al portapapeles';
|
||||||
@override String get relayUnreachable => 'El servidor de retransmisión no está disponible. Esto puede deberse a que tu proveedor de internet bloquea la conexión. Puedes intentarlo de todos modos, pero Watch Together podría no funcionar.';
|
@override String get relayUnreachable => 'El servidor de retransmisión no está disponible. Esto puede deberse a que tu proveedor de internet bloquea la conexión. Puedes intentarlo de todos modos, pero Watch Together podría no funcionar.';
|
||||||
|
@override String get reconnectingToHost => 'Reconectando con el anfitrión...';
|
||||||
|
@override String participantJoined({required Object name}) => '${name} se unió';
|
||||||
|
@override String participantLeft({required Object name}) => '${name} se fue';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: downloads
|
// Path: downloads
|
||||||
@@ -1699,6 +1702,9 @@ extension on TranslationsEs {
|
|||||||
'watchTogether.failedToJoin' => 'Error al unirse a la sesión',
|
'watchTogether.failedToJoin' => 'Error al unirse a la sesión',
|
||||||
'watchTogether.sessionCodeCopied' => 'Código de sesión copiado al portapapeles',
|
'watchTogether.sessionCodeCopied' => 'Código de sesión copiado al portapapeles',
|
||||||
'watchTogether.relayUnreachable' => 'El servidor de retransmisión no está disponible. Esto puede deberse a que tu proveedor de internet bloquea la conexión. Puedes intentarlo de todos modos, pero Watch Together podría no funcionar.',
|
'watchTogether.relayUnreachable' => 'El servidor de retransmisión no está disponible. Esto puede deberse a que tu proveedor de internet bloquea la conexión. Puedes intentarlo de todos modos, pero Watch Together podría no funcionar.',
|
||||||
|
'watchTogether.reconnectingToHost' => 'Reconectando con el anfitrión...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name} se unió',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name} se fue',
|
||||||
'downloads.title' => 'Descargas',
|
'downloads.title' => 'Descargas',
|
||||||
'downloads.manage' => 'Gestionar',
|
'downloads.manage' => 'Gestionar',
|
||||||
'downloads.tvShows' => 'Series de TV',
|
'downloads.tvShows' => 'Series de TV',
|
||||||
|
|||||||
@@ -878,6 +878,9 @@ class _TranslationsWatchTogetherFr implements TranslationsWatchTogetherEn {
|
|||||||
@override String get failedToJoin => 'Échec de la connexion à la session';
|
@override String get failedToJoin => 'Échec de la connexion à la session';
|
||||||
@override String get sessionCodeCopied => 'Code de session copié dans le presse-papiers';
|
@override String get sessionCodeCopied => 'Code de session copié dans le presse-papiers';
|
||||||
@override String get relayUnreachable => 'Le serveur relais est inaccessible. Cela peut être dû au blocage de la connexion par votre fournisseur d\'accès. Vous pouvez quand même essayer, mais Watch Together pourrait ne pas fonctionner.';
|
@override String get relayUnreachable => 'Le serveur relais est inaccessible. Cela peut être dû au blocage de la connexion par votre fournisseur d\'accès. Vous pouvez quand même essayer, mais Watch Together pourrait ne pas fonctionner.';
|
||||||
|
@override String get reconnectingToHost => 'Reconnexion à l\'hôte...';
|
||||||
|
@override String participantJoined({required Object name}) => '${name} a rejoint';
|
||||||
|
@override String participantLeft({required Object name}) => '${name} est parti';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: downloads
|
// Path: downloads
|
||||||
@@ -1699,6 +1702,9 @@ extension on TranslationsFr {
|
|||||||
'watchTogether.failedToJoin' => 'Échec de la connexion à la session',
|
'watchTogether.failedToJoin' => 'Échec de la connexion à la session',
|
||||||
'watchTogether.sessionCodeCopied' => 'Code de session copié dans le presse-papiers',
|
'watchTogether.sessionCodeCopied' => 'Code de session copié dans le presse-papiers',
|
||||||
'watchTogether.relayUnreachable' => 'Le serveur relais est inaccessible. Cela peut être dû au blocage de la connexion par votre fournisseur d\'accès. Vous pouvez quand même essayer, mais Watch Together pourrait ne pas fonctionner.',
|
'watchTogether.relayUnreachable' => 'Le serveur relais est inaccessible. Cela peut être dû au blocage de la connexion par votre fournisseur d\'accès. Vous pouvez quand même essayer, mais Watch Together pourrait ne pas fonctionner.',
|
||||||
|
'watchTogether.reconnectingToHost' => 'Reconnexion à l\'hôte...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name} a rejoint',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name} est parti',
|
||||||
'downloads.title' => 'Téléchargements',
|
'downloads.title' => 'Téléchargements',
|
||||||
'downloads.manage' => 'Gérer',
|
'downloads.manage' => 'Gérer',
|
||||||
'downloads.tvShows' => 'Show TV',
|
'downloads.tvShows' => 'Show TV',
|
||||||
|
|||||||
@@ -905,6 +905,9 @@ class _TranslationsWatchTogetherIt implements TranslationsWatchTogetherEn {
|
|||||||
@override String get failedToJoin => 'Impossibile unirsi alla sessione';
|
@override String get failedToJoin => 'Impossibile unirsi alla sessione';
|
||||||
@override String get sessionCodeCopied => 'Codice sessione copiato negli appunti';
|
@override String get sessionCodeCopied => 'Codice sessione copiato negli appunti';
|
||||||
@override String get relayUnreachable => 'Il server di inoltro non è raggiungibile. Questo potrebbe essere causato dal blocco della connessione da parte del tuo provider. Puoi comunque provare, ma Watch Together potrebbe non funzionare.';
|
@override String get relayUnreachable => 'Il server di inoltro non è raggiungibile. Questo potrebbe essere causato dal blocco della connessione da parte del tuo provider. Puoi comunque provare, ma Watch Together potrebbe non funzionare.';
|
||||||
|
@override String get reconnectingToHost => 'Riconnessione all\'host...';
|
||||||
|
@override String participantJoined({required Object name}) => '${name} si è unito';
|
||||||
|
@override String participantLeft({required Object name}) => '${name} se ne è andato';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: shaders
|
// Path: shaders
|
||||||
@@ -1717,6 +1720,9 @@ extension on TranslationsIt {
|
|||||||
'watchTogether.failedToJoin' => 'Impossibile unirsi alla sessione',
|
'watchTogether.failedToJoin' => 'Impossibile unirsi alla sessione',
|
||||||
'watchTogether.sessionCodeCopied' => 'Codice sessione copiato negli appunti',
|
'watchTogether.sessionCodeCopied' => 'Codice sessione copiato negli appunti',
|
||||||
'watchTogether.relayUnreachable' => 'Il server di inoltro non è raggiungibile. Questo potrebbe essere causato dal blocco della connessione da parte del tuo provider. Puoi comunque provare, ma Watch Together potrebbe non funzionare.',
|
'watchTogether.relayUnreachable' => 'Il server di inoltro non è raggiungibile. Questo potrebbe essere causato dal blocco della connessione da parte del tuo provider. Puoi comunque provare, ma Watch Together potrebbe non funzionare.',
|
||||||
|
'watchTogether.reconnectingToHost' => 'Riconnessione all\'host...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name} si è unito',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name} se ne è andato',
|
||||||
'shaders.title' => 'Shader',
|
'shaders.title' => 'Shader',
|
||||||
'shaders.noShaderDescription' => 'Nessun miglioramento video',
|
'shaders.noShaderDescription' => 'Nessun miglioramento video',
|
||||||
'shaders.nvscalerDescription' => 'Ridimensionamento NVIDIA per video più nitido',
|
'shaders.nvscalerDescription' => 'Ridimensionamento NVIDIA per video più nitido',
|
||||||
|
|||||||
@@ -878,6 +878,9 @@ class _TranslationsWatchTogetherKo implements TranslationsWatchTogetherEn {
|
|||||||
@override String get failedToJoin => '세션 참여 실패';
|
@override String get failedToJoin => '세션 참여 실패';
|
||||||
@override String get sessionCodeCopied => '세션 코드가 클립보드에 복사되었습니다';
|
@override String get sessionCodeCopied => '세션 코드가 클립보드에 복사되었습니다';
|
||||||
@override String get relayUnreachable => '릴레이 서버에 연결할 수 없습니다. 인터넷 제공업체가 연결을 차단하고 있을 수 있습니다. 시도해 볼 수 있지만 함께 보기가 작동하지 않을 수 있습니다.';
|
@override String get relayUnreachable => '릴레이 서버에 연결할 수 없습니다. 인터넷 제공업체가 연결을 차단하고 있을 수 있습니다. 시도해 볼 수 있지만 함께 보기가 작동하지 않을 수 있습니다.';
|
||||||
|
@override String get reconnectingToHost => '호스트에 재연결 중...';
|
||||||
|
@override String participantJoined({required Object name}) => '${name}님이 참여했습니다';
|
||||||
|
@override String participantLeft({required Object name}) => '${name}님이 나갔습니다';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: downloads
|
// Path: downloads
|
||||||
@@ -1699,6 +1702,9 @@ extension on TranslationsKo {
|
|||||||
'watchTogether.failedToJoin' => '세션 참여 실패',
|
'watchTogether.failedToJoin' => '세션 참여 실패',
|
||||||
'watchTogether.sessionCodeCopied' => '세션 코드가 클립보드에 복사되었습니다',
|
'watchTogether.sessionCodeCopied' => '세션 코드가 클립보드에 복사되었습니다',
|
||||||
'watchTogether.relayUnreachable' => '릴레이 서버에 연결할 수 없습니다. 인터넷 제공업체가 연결을 차단하고 있을 수 있습니다. 시도해 볼 수 있지만 함께 보기가 작동하지 않을 수 있습니다.',
|
'watchTogether.relayUnreachable' => '릴레이 서버에 연결할 수 없습니다. 인터넷 제공업체가 연결을 차단하고 있을 수 있습니다. 시도해 볼 수 있지만 함께 보기가 작동하지 않을 수 있습니다.',
|
||||||
|
'watchTogether.reconnectingToHost' => '호스트에 재연결 중...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name}님이 참여했습니다',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name}님이 나갔습니다',
|
||||||
'downloads.title' => '다운로드',
|
'downloads.title' => '다운로드',
|
||||||
'downloads.manage' => '관리',
|
'downloads.manage' => '관리',
|
||||||
'downloads.tvShows' => 'TV 프로그램',
|
'downloads.tvShows' => 'TV 프로그램',
|
||||||
|
|||||||
@@ -905,6 +905,9 @@ class _TranslationsWatchTogetherNl implements TranslationsWatchTogetherEn {
|
|||||||
@override String get failedToJoin => 'Sessie deelnemen mislukt';
|
@override String get failedToJoin => 'Sessie deelnemen mislukt';
|
||||||
@override String get sessionCodeCopied => 'Sessiecode gekopieerd naar klembord';
|
@override String get sessionCodeCopied => 'Sessiecode gekopieerd naar klembord';
|
||||||
@override String get relayUnreachable => 'De relayserver is niet bereikbaar. Dit kan worden veroorzaakt doordat je internetprovider de verbinding blokkeert. Je kunt het toch proberen, maar Watch Together werkt mogelijk niet.';
|
@override String get relayUnreachable => 'De relayserver is niet bereikbaar. Dit kan worden veroorzaakt doordat je internetprovider de verbinding blokkeert. Je kunt het toch proberen, maar Watch Together werkt mogelijk niet.';
|
||||||
|
@override String get reconnectingToHost => 'Opnieuw verbinden met host...';
|
||||||
|
@override String participantJoined({required Object name}) => '${name} is toegetreden';
|
||||||
|
@override String participantLeft({required Object name}) => '${name} heeft de sessie verlaten';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: shaders
|
// Path: shaders
|
||||||
@@ -1717,6 +1720,9 @@ extension on TranslationsNl {
|
|||||||
'watchTogether.failedToJoin' => 'Sessie deelnemen mislukt',
|
'watchTogether.failedToJoin' => 'Sessie deelnemen mislukt',
|
||||||
'watchTogether.sessionCodeCopied' => 'Sessiecode gekopieerd naar klembord',
|
'watchTogether.sessionCodeCopied' => 'Sessiecode gekopieerd naar klembord',
|
||||||
'watchTogether.relayUnreachable' => 'De relayserver is niet bereikbaar. Dit kan worden veroorzaakt doordat je internetprovider de verbinding blokkeert. Je kunt het toch proberen, maar Watch Together werkt mogelijk niet.',
|
'watchTogether.relayUnreachable' => 'De relayserver is niet bereikbaar. Dit kan worden veroorzaakt doordat je internetprovider de verbinding blokkeert. Je kunt het toch proberen, maar Watch Together werkt mogelijk niet.',
|
||||||
|
'watchTogether.reconnectingToHost' => 'Opnieuw verbinden met host...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name} is toegetreden',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name} heeft de sessie verlaten',
|
||||||
'shaders.title' => 'Shaders',
|
'shaders.title' => 'Shaders',
|
||||||
'shaders.noShaderDescription' => 'Geen videoverbetering',
|
'shaders.noShaderDescription' => 'Geen videoverbetering',
|
||||||
'shaders.nvscalerDescription' => 'NVIDIA-beeldschaling voor scherpere video',
|
'shaders.nvscalerDescription' => 'NVIDIA-beeldschaling voor scherpere video',
|
||||||
|
|||||||
@@ -905,6 +905,9 @@ class _TranslationsWatchTogetherSv implements TranslationsWatchTogetherEn {
|
|||||||
@override String get failedToJoin => 'Det gick inte att gå med i session';
|
@override String get failedToJoin => 'Det gick inte att gå med i session';
|
||||||
@override String get sessionCodeCopied => 'Sessionskod kopierad till urklipp';
|
@override String get sessionCodeCopied => 'Sessionskod kopierad till urklipp';
|
||||||
@override String get relayUnreachable => 'Reläservern kan inte nås. Detta kan bero på att din internetleverantör blockerar anslutningen. Du kan fortfarande försöka, men Watch Together kanske inte fungerar.';
|
@override String get relayUnreachable => 'Reläservern kan inte nås. Detta kan bero på att din internetleverantör blockerar anslutningen. Du kan fortfarande försöka, men Watch Together kanske inte fungerar.';
|
||||||
|
@override String get reconnectingToHost => 'Återansluter till värd...';
|
||||||
|
@override String participantJoined({required Object name}) => '${name} gick med';
|
||||||
|
@override String participantLeft({required Object name}) => '${name} lämnade';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: shaders
|
// Path: shaders
|
||||||
@@ -1717,6 +1720,9 @@ extension on TranslationsSv {
|
|||||||
'watchTogether.failedToJoin' => 'Det gick inte att gå med i session',
|
'watchTogether.failedToJoin' => 'Det gick inte att gå med i session',
|
||||||
'watchTogether.sessionCodeCopied' => 'Sessionskod kopierad till urklipp',
|
'watchTogether.sessionCodeCopied' => 'Sessionskod kopierad till urklipp',
|
||||||
'watchTogether.relayUnreachable' => 'Reläservern kan inte nås. Detta kan bero på att din internetleverantör blockerar anslutningen. Du kan fortfarande försöka, men Watch Together kanske inte fungerar.',
|
'watchTogether.relayUnreachable' => 'Reläservern kan inte nås. Detta kan bero på att din internetleverantör blockerar anslutningen. Du kan fortfarande försöka, men Watch Together kanske inte fungerar.',
|
||||||
|
'watchTogether.reconnectingToHost' => 'Återansluter till värd...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name} gick med',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name} lämnade',
|
||||||
'shaders.title' => 'Shaders',
|
'shaders.title' => 'Shaders',
|
||||||
'shaders.noShaderDescription' => 'Ingen videoförbättring',
|
'shaders.noShaderDescription' => 'Ingen videoförbättring',
|
||||||
'shaders.nvscalerDescription' => 'NVIDIA-bildskalning för skarpare video',
|
'shaders.nvscalerDescription' => 'NVIDIA-bildskalning för skarpare video',
|
||||||
|
|||||||
@@ -905,6 +905,9 @@ class _TranslationsWatchTogetherZh implements TranslationsWatchTogetherEn {
|
|||||||
@override String get failedToJoin => '加入会话失败';
|
@override String get failedToJoin => '加入会话失败';
|
||||||
@override String get sessionCodeCopied => '会话代码已复制到剪贴板';
|
@override String get sessionCodeCopied => '会话代码已复制到剪贴板';
|
||||||
@override String get relayUnreachable => '无法连接到中继服务器。这可能是由于您的网络运营商屏蔽了连接。您仍然可以尝试,但一起观看功能可能无法正常使用。';
|
@override String get relayUnreachable => '无法连接到中继服务器。这可能是由于您的网络运营商屏蔽了连接。您仍然可以尝试,但一起观看功能可能无法正常使用。';
|
||||||
|
@override String get reconnectingToHost => '正在重新连接到主持人...';
|
||||||
|
@override String participantJoined({required Object name}) => '${name} 加入了';
|
||||||
|
@override String participantLeft({required Object name}) => '${name} 离开了';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: shaders
|
// Path: shaders
|
||||||
@@ -1717,6 +1720,9 @@ extension on TranslationsZh {
|
|||||||
'watchTogether.failedToJoin' => '加入会话失败',
|
'watchTogether.failedToJoin' => '加入会话失败',
|
||||||
'watchTogether.sessionCodeCopied' => '会话代码已复制到剪贴板',
|
'watchTogether.sessionCodeCopied' => '会话代码已复制到剪贴板',
|
||||||
'watchTogether.relayUnreachable' => '无法连接到中继服务器。这可能是由于您的网络运营商屏蔽了连接。您仍然可以尝试,但一起观看功能可能无法正常使用。',
|
'watchTogether.relayUnreachable' => '无法连接到中继服务器。这可能是由于您的网络运营商屏蔽了连接。您仍然可以尝试,但一起观看功能可能无法正常使用。',
|
||||||
|
'watchTogether.reconnectingToHost' => '正在重新连接到主持人...',
|
||||||
|
'watchTogether.participantJoined' => ({required Object name}) => '${name} 加入了',
|
||||||
|
'watchTogether.participantLeft' => ({required Object name}) => '${name} 离开了',
|
||||||
'shaders.title' => '着色器',
|
'shaders.title' => '着色器',
|
||||||
'shaders.noShaderDescription' => '无视频增强',
|
'shaders.noShaderDescription' => '无视频增强',
|
||||||
'shaders.nvscalerDescription' => 'NVIDIA 图像缩放,使视频更清晰',
|
'shaders.nvscalerDescription' => 'NVIDIA 图像缩放,使视频更清晰',
|
||||||
|
|||||||
@@ -644,7 +644,10 @@
|
|||||||
"failedToCreate": "Det gick inte att skapa session",
|
"failedToCreate": "Det gick inte att skapa session",
|
||||||
"failedToJoin": "Det gick inte att gå med i session",
|
"failedToJoin": "Det gick inte att gå med i session",
|
||||||
"sessionCodeCopied": "Sessionskod kopierad till urklipp",
|
"sessionCodeCopied": "Sessionskod kopierad till urklipp",
|
||||||
"relayUnreachable": "Reläservern kan inte nås. Detta kan bero på att din internetleverantör blockerar anslutningen. Du kan fortfarande försöka, men Watch Together kanske inte fungerar."
|
"relayUnreachable": "Reläservern kan inte nås. Detta kan bero på att din internetleverantör blockerar anslutningen. Du kan fortfarande försöka, men Watch Together kanske inte fungerar.",
|
||||||
|
"reconnectingToHost": "Återansluter till värd...",
|
||||||
|
"participantJoined": "${name} gick med",
|
||||||
|
"participantLeft": "${name} lämnade"
|
||||||
},
|
},
|
||||||
"shaders": {
|
"shaders": {
|
||||||
"title": "Shaders",
|
"title": "Shaders",
|
||||||
|
|||||||
@@ -644,7 +644,10 @@
|
|||||||
"failedToCreate": "创建会话失败",
|
"failedToCreate": "创建会话失败",
|
||||||
"failedToJoin": "加入会话失败",
|
"failedToJoin": "加入会话失败",
|
||||||
"sessionCodeCopied": "会话代码已复制到剪贴板",
|
"sessionCodeCopied": "会话代码已复制到剪贴板",
|
||||||
"relayUnreachable": "无法连接到中继服务器。这可能是由于您的网络运营商屏蔽了连接。您仍然可以尝试,但一起观看功能可能无法正常使用。"
|
"relayUnreachable": "无法连接到中继服务器。这可能是由于您的网络运营商屏蔽了连接。您仍然可以尝试,但一起观看功能可能无法正常使用。",
|
||||||
|
"reconnectingToHost": "正在重新连接到主持人...",
|
||||||
|
"participantJoined": "${name} 加入了",
|
||||||
|
"participantLeft": "${name} 离开了"
|
||||||
},
|
},
|
||||||
"shaders": {
|
"shaders": {
|
||||||
"title": "着色器",
|
"title": "着色器",
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ class ProfileSwitchScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ProfileSwitchScreenState extends State<ProfileSwitchScreen> {
|
class _ProfileSwitchScreenState extends State<ProfileSwitchScreen> {
|
||||||
bool _allowPop = false;
|
bool _allowPop = false;
|
||||||
|
final FocusNode _firstSelectableFocusNode = FocusNode();
|
||||||
|
bool _focusRequested = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_firstSelectableFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -77,10 +85,18 @@ class _ProfileSwitchScreenState extends State<ProfileSwitchScreen> {
|
|||||||
final isFirstSelectable =
|
final isFirstSelectable =
|
||||||
!isCurrentUser && !users.take(index).any((u) => u.uuid != userProvider.currentUser?.uuid);
|
!isCurrentUser && !users.take(index).any((u) => u.uuid != userProvider.currentUser?.uuid);
|
||||||
|
|
||||||
|
if (isFirstSelectable && !_focusRequested) {
|
||||||
|
_focusRequested = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) _firstSelectableFocusNode.requestFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(left: 16, right: 16, top: index == 0 ? 16 : 0, bottom: 8),
|
padding: EdgeInsets.only(left: 16, right: 16, top: index == 0 ? 16 : 0, bottom: 8),
|
||||||
child: FocusableWrapper(
|
child: FocusableWrapper(
|
||||||
autofocus: isFirstSelectable,
|
autofocus: isFirstSelectable,
|
||||||
|
focusNode: isFirstSelectable ? _firstSelectableFocusNode : null,
|
||||||
disableScale: true,
|
disableScale: true,
|
||||||
onSelect: isCurrentUser ? null : () => _switchToUser(context, user),
|
onSelect: isCurrentUser ? null : () => _switchToUser(context, user),
|
||||||
child: Card(
|
child: Card(
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import '../focus/dpad_navigator.dart';
|
|||||||
import '../focus/key_event_utils.dart';
|
import '../focus/key_event_utils.dart';
|
||||||
import '../i18n/strings.g.dart';
|
import '../i18n/strings.g.dart';
|
||||||
import '../watch_together/providers/watch_together_provider.dart';
|
import '../watch_together/providers/watch_together_provider.dart';
|
||||||
|
import '../watch_together/widgets/watch_together_overlay.dart';
|
||||||
|
|
||||||
class VideoPlayerScreen extends StatefulWidget {
|
class VideoPlayerScreen extends StatefulWidget {
|
||||||
final PlexMetadata metadata;
|
final PlexMetadata metadata;
|
||||||
@@ -1986,13 +1987,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Compute canControl from Watch Together provider
|
// Compute canControl from Watch Together provider (reactive)
|
||||||
bool canControl = true;
|
bool canControl = true;
|
||||||
try {
|
try {
|
||||||
final watchTogether = this.context.read<WatchTogetherProvider>();
|
canControl = context.select<WatchTogetherProvider, bool>(
|
||||||
if (watchTogether.isInSession) {
|
(wt) => wt.isInSession ? wt.canControl() : true,
|
||||||
canControl = watchTogether.canControl();
|
);
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Watch Together not available, default to can control
|
// Watch Together not available, default to can control
|
||||||
}
|
}
|
||||||
@@ -2249,6 +2249,37 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Watch Together: reconnecting to host overlay
|
||||||
|
Consumer<WatchTogetherProvider>(
|
||||||
|
builder: (context, provider, child) {
|
||||||
|
if (!provider.isWaitingForHostReconnect) return const SizedBox.shrink();
|
||||||
|
return Positioned(
|
||||||
|
bottom: 120,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(color: Colors.black54, borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(t.watchTogether.reconnectingToHost, style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Watch Together: participant join/leave notifications
|
||||||
|
const ParticipantNotificationOverlay(),
|
||||||
// Black overlay during exit (no spinner - just covers transparency)
|
// Black overlay during exit (no spinner - just covers transparency)
|
||||||
ValueListenableBuilder<bool>(
|
ValueListenableBuilder<bool>(
|
||||||
valueListenable: _isExiting,
|
valueListenable: _isExiting,
|
||||||
|
|||||||
@@ -810,7 +810,7 @@ class SettingsService extends BaseSharedPreferencesService {
|
|||||||
|
|
||||||
AppLocale getAppLocale() {
|
AppLocale getAppLocale() {
|
||||||
final localeString = prefs.getString(_keyAppLocale);
|
final localeString = prefs.getString(_keyAppLocale);
|
||||||
if (localeString == null) return AppLocale.en; // Default to English
|
if (localeString == null) return AppLocaleUtils.findDeviceLocale();
|
||||||
|
|
||||||
return AppLocale.values.firstWhere((locale) => locale.languageCode == localeString, orElse: () => AppLocale.en);
|
return AppLocale.values.firstWhere((locale) => locale.languageCode == localeString, orElse: () => AppLocale.en);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
String _displayName = 'User';
|
String _displayName = 'User';
|
||||||
|
|
||||||
|
// Host reconnect grace period
|
||||||
|
Timer? _hostReconnectTimer;
|
||||||
|
bool _isWaitingForHostReconnect = false;
|
||||||
|
|
||||||
/// Generate a random display name for this session
|
/// Generate a random display name for this session
|
||||||
static String _generateDisplayName() {
|
static String _generateDisplayName() {
|
||||||
const adjectives = ['Happy', 'Sleepy', 'Sunny', 'Cozy', 'Chill', 'Swift', 'Brave', 'Calm', 'Jolly', 'Lucky'];
|
const adjectives = ['Happy', 'Sleepy', 'Sunny', 'Cozy', 'Chill', 'Swift', 'Brave', 'Calm', 'Jolly', 'Lucky'];
|
||||||
@@ -65,6 +69,11 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
ControlMode get controlMode => _session?.controlMode ?? ControlMode.hostOnly;
|
ControlMode get controlMode => _session?.controlMode ?? ControlMode.hostOnly;
|
||||||
String? get sessionId => _session?.sessionId;
|
String? get sessionId => _session?.sessionId;
|
||||||
WatchTogetherSyncManager? get syncManager => _syncManager;
|
WatchTogetherSyncManager? get syncManager => _syncManager;
|
||||||
|
bool get isWaitingForHostReconnect => _isWaitingForHostReconnect;
|
||||||
|
|
||||||
|
// Participant join/leave event stream
|
||||||
|
final StreamController<ParticipantEvent> _participantEventController = StreamController<ParticipantEvent>.broadcast();
|
||||||
|
Stream<ParticipantEvent> get participantEvents => _participantEventController.stream;
|
||||||
|
|
||||||
// Current media getters
|
// Current media getters
|
||||||
String? get currentMediaRatingKey => _session?.mediaRatingKey;
|
String? get currentMediaRatingKey => _session?.mediaRatingKey;
|
||||||
@@ -207,6 +216,9 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
_messageSubscription = null;
|
_messageSubscription = null;
|
||||||
_errorSubscription = null;
|
_errorSubscription = null;
|
||||||
|
|
||||||
|
// Cancel host reconnect grace period
|
||||||
|
_cancelHostReconnectGracePeriod();
|
||||||
|
|
||||||
// Clean up services
|
// Clean up services
|
||||||
_syncManager?.dispose();
|
_syncManager?.dispose();
|
||||||
_syncManager = null;
|
_syncManager = null;
|
||||||
@@ -248,19 +260,29 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
void _setupPeerServiceListeners() {
|
void _setupPeerServiceListeners() {
|
||||||
_peerConnectedSubscription = _peerService!.onPeerConnected.listen((peerId) {
|
_peerConnectedSubscription = _peerService!.onPeerConnected.listen((peerId) {
|
||||||
appLogger.d('WatchTogether: Peer connected: $peerId');
|
appLogger.d('WatchTogether: Peer connected: $peerId');
|
||||||
|
|
||||||
|
// If host reconnected during grace period, cancel the timer
|
||||||
|
if (!isHost && peerId == _session?.hostPeerId && _isWaitingForHostReconnect) {
|
||||||
|
_cancelHostReconnectGracePeriod();
|
||||||
|
}
|
||||||
|
|
||||||
// Peer will announce themselves with a join message
|
// Peer will announce themselves with a join message
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
_peerDisconnectedSubscription = _peerService!.onPeerDisconnected.listen((peerId) {
|
_peerDisconnectedSubscription = _peerService!.onPeerDisconnected.listen((peerId) {
|
||||||
appLogger.d('WatchTogether: Peer disconnected: $peerId');
|
appLogger.d('WatchTogether: Peer disconnected: $peerId');
|
||||||
|
|
||||||
|
// Capture display name before removal for notification
|
||||||
|
final disconnectedName = _participants.where((p) => p.peerId == peerId).map((p) => p.displayName).firstOrNull;
|
||||||
|
|
||||||
_participants.removeWhere((p) => p.peerId == peerId);
|
_participants.removeWhere((p) => p.peerId == peerId);
|
||||||
|
|
||||||
// If host disconnected, end session for guests
|
// If host disconnected, start grace period for reconnection
|
||||||
if (!isHost && peerId == _session?.hostPeerId) {
|
if (!isHost && peerId == _session?.hostPeerId) {
|
||||||
_session = _session?.copyWith(state: SessionState.error, errorMessage: 'Host left the session');
|
_startHostReconnectGracePeriod();
|
||||||
// Ensure guests exit the player if host disappears
|
} else if (disconnectedName != null) {
|
||||||
onHostExitedPlayer?.call();
|
_participantEventController.add(ParticipantEvent(displayName: disconnectedName, type: ParticipantEventType.left));
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -300,6 +322,9 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
_participants.add(
|
_participants.add(
|
||||||
Participant(peerId: message.peerId!, displayName: message.displayName!, isHost: message.isHost ?? false),
|
Participant(peerId: message.peerId!, displayName: message.displayName!, isHost: message.isHost ?? false),
|
||||||
);
|
);
|
||||||
|
_participantEventController.add(
|
||||||
|
ParticipantEvent(displayName: message.displayName!, type: ParticipantEventType.joined),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're the host, send our join info back so the new peer
|
// If we're the host, send our join info back so the new peer
|
||||||
@@ -319,7 +344,11 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
|
|
||||||
case SyncMessageType.leave:
|
case SyncMessageType.leave:
|
||||||
if (message.peerId != null) {
|
if (message.peerId != null) {
|
||||||
|
final leavingName = _participants.where((p) => p.peerId == message.peerId).map((p) => p.displayName).firstOrNull;
|
||||||
_participants.removeWhere((p) => p.peerId == message.peerId);
|
_participants.removeWhere((p) => p.peerId == message.peerId);
|
||||||
|
if (leavingName != null) {
|
||||||
|
_participantEventController.add(ParticipantEvent(displayName: leavingName, type: ParticipantEventType.left));
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -512,9 +541,51 @@ class WatchTogetherProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start a grace period for host reconnection (guest only)
|
||||||
|
void _startHostReconnectGracePeriod() {
|
||||||
|
_cancelHostReconnectGracePeriod();
|
||||||
|
_isWaitingForHostReconnect = true;
|
||||||
|
appLogger.d('WatchTogether: Host disconnected, waiting 15s for reconnection');
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
_hostReconnectTimer = Timer(const Duration(seconds: 15), () {
|
||||||
|
if (_isWaitingForHostReconnect) {
|
||||||
|
appLogger.d('WatchTogether: Host reconnect grace period expired');
|
||||||
|
_isWaitingForHostReconnect = false;
|
||||||
|
_session = _session?.copyWith(state: SessionState.error, errorMessage: 'Host left the session');
|
||||||
|
onHostExitedPlayer?.call();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel host reconnect grace period
|
||||||
|
void _cancelHostReconnectGracePeriod() {
|
||||||
|
_hostReconnectTimer?.cancel();
|
||||||
|
_hostReconnectTimer = null;
|
||||||
|
if (_isWaitingForHostReconnect) {
|
||||||
|
_isWaitingForHostReconnect = false;
|
||||||
|
appLogger.d('WatchTogether: Host reconnected, grace period cancelled');
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_cancelHostReconnectGracePeriod();
|
||||||
|
_participantEventController.close();
|
||||||
leaveSession();
|
leaveSession();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Type of participant event
|
||||||
|
enum ParticipantEventType { joined, left }
|
||||||
|
|
||||||
|
/// Event emitted when a participant joins or leaves
|
||||||
|
class ParticipantEvent {
|
||||||
|
final String displayName;
|
||||||
|
final ParticipantEventType type;
|
||||||
|
|
||||||
|
const ParticipantEvent({required this.displayName, required this.type});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@@ -276,6 +278,86 @@ class _SessionMenuSheet extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Auto-dismissing toast for participant join/leave events
|
||||||
|
class ParticipantNotificationOverlay extends StatefulWidget {
|
||||||
|
const ParticipantNotificationOverlay({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ParticipantNotificationOverlay> createState() => _ParticipantNotificationOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParticipantNotificationOverlayState extends State<ParticipantNotificationOverlay> {
|
||||||
|
StreamSubscription<ParticipantEvent>? _subscription;
|
||||||
|
final List<_NotificationEntry> _notifications = [];
|
||||||
|
int _nextId = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_subscription?.cancel();
|
||||||
|
try {
|
||||||
|
final provider = context.read<WatchTogetherProvider>();
|
||||||
|
_subscription = provider.participantEvents.listen(_onEvent);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEvent(ParticipantEvent event) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final id = _nextId++;
|
||||||
|
setState(() {
|
||||||
|
_notifications.add(_NotificationEntry(id: id, event: event));
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(seconds: 3), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_notifications.removeWhere((n) => n.id == id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_notifications.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
top: 60,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: _notifications.map((n) {
|
||||||
|
final text = n.event.type == ParticipantEventType.joined
|
||||||
|
? t.watchTogether.participantJoined(name: n.event.displayName)
|
||||||
|
: t.watchTogether.participantLeft(name: n.event.displayName);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(color: Colors.black54, borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Text(text, style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationEntry {
|
||||||
|
final int id;
|
||||||
|
final ParticipantEvent event;
|
||||||
|
const _NotificationEntry({required this.id, required this.event});
|
||||||
|
}
|
||||||
|
|
||||||
/// Compact sync indicator for showing during drift correction
|
/// Compact sync indicator for showing during drift correction
|
||||||
class SyncingIndicator extends StatelessWidget {
|
class SyncingIndicator extends StatelessWidget {
|
||||||
const SyncingIndicator({super.key});
|
const SyncingIndicator({super.key});
|
||||||
|
|||||||
@@ -627,6 +627,11 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
|||||||
if (Platform.isMacOS) {
|
if (Platform.isMacOS) {
|
||||||
_updateTrafficLightVisibility();
|
_updateTrafficLightVisibility();
|
||||||
}
|
}
|
||||||
|
// Immediately try to reclaim focus (important for TV where global handler
|
||||||
|
// won't fire if _focusNode lost focus)
|
||||||
|
if (!_focusNode.hasFocus) {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted && !_focusNode.hasFocus) {
|
if (mounted && !_focusNode.hasFocus) {
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
@@ -1334,6 +1339,20 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
|||||||
bool _handleGlobalKeyEvent(KeyEvent event) {
|
bool _handleGlobalKeyEvent(KeyEvent event) {
|
||||||
if (!mounted) return false;
|
if (!mounted) return false;
|
||||||
|
|
||||||
|
// TV back key fallback — Focus.onKeyEvent won't fire if _focusNode lost focus
|
||||||
|
if (PlatformDetector.isTV() && event.logicalKey.isBackKey) {
|
||||||
|
if (!_focusNode.hasFocus) {
|
||||||
|
final backResult = handleBackKeyAction(event, () {
|
||||||
|
if (!_showControls) {
|
||||||
|
_showControlsWithFocus();
|
||||||
|
} else {
|
||||||
|
(widget.onBack ?? () => Navigator.of(context).pop(true))();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (backResult != KeyEventResult.ignored) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only handle when video player navigation is disabled (desktop mode without D-pad nav)
|
// Only handle when video player navigation is disabled (desktop mode without D-pad nav)
|
||||||
if (_videoPlayerNavigationEnabled) return false;
|
if (_videoPlayerNavigationEnabled) return false;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user