diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c40fe889..48644089 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -30,6 +30,13 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + + if (System.getenv("AMAZON") != null) { + versionCode = (flutter.versionCode ?: 0) + 3000 + ndk { + abiFilters += listOf("armeabi-v7a", "arm64-v8a") + } + } } signingConfigs { diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 5bfd31a1..c84e39eb 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -22,17 +22,17 @@ platform :android do debug_info_dir = "./debug-info/#{version_name}+#{version_code}" # Build the Flutter app - sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true --obfuscate --split-debug-info=#{debug_info_dir}/aab") + # sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true --obfuscate --split-debug-info=#{debug_info_dir}/aab") - upload_to_play_store( - track: "production", - release_status: "completed", - version_code: version_code, - aab: "../build/app/outputs/bundle/release/app-release.aab" - ) + # upload_to_play_store( + # track: "production", + # release_status: "completed", + # version_code: version_code, + # aab: "../build/app/outputs/bundle/release/app-release.aab" + # ) - # Build universal APK for Amazon Appstore (doesn't accept AABs, doesn't auto-target by ABI) - sh("cd #{ENV['PWD']}/.. && flutter build apk --release --target-platform android-arm,android-arm64 --obfuscate --split-debug-info=#{debug_info_dir}/apk") + # Build universal APK for Amazon Appstore (doesn't accept AABs, excludes x86 via env var) + sh("cd #{ENV['PWD']}/.. && AMAZON=1 flutter build apk --release --obfuscate --split-debug-info=#{debug_info_dir}/apk") upload_to_amazon_appstore( apk_paths: [ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 15e44fbf..ef5568cf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -66,6 +66,11 @@ PODS: - Flutter - FlutterMacOS - PromisesObjC (2.4.0) + - Sentry/HybridSDK (8.46.0) + - sentry_flutter (8.14.2): + - Flutter + - FlutterMacOS + - Sentry/HybridSDK (= 8.46.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -117,6 +122,7 @@ DEPENDENCIES: - os_media_controls (from `.symlinks/plugins/os_media_controls/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) @@ -138,6 +144,7 @@ SPEC REPOS: - MLKitVision - nanopb - PromisesObjC + - Sentry - sqlite3 EXTERNAL SOURCES: @@ -161,6 +168,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + sentry_flutter: + :path: ".symlinks/plugins/sentry_flutter/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite_darwin: @@ -198,6 +207,8 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 + sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 784bcf46..454283d5 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "Tastenkürzel anpassen", "videoPlayerNavigation": "Videoplayer-Navigation", "videoPlayerNavigationDescription": "Pfeiltasten zur Navigation der Videoplayer-Steuerung verwenden", + "crashReporting": "Absturzberichte", + "crashReportingDescription": "Absturzberichte senden, um die App zu verbessern", "debugLogging": "Debug-Protokollierung", "debugLoggingDescription": "Detaillierte Protokolle zur Fehleranalyse aktivieren", "viewLogs": "Protokolle anzeigen", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 769d8fe4..b309361f 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "Customize keyboard shortcuts", "videoPlayerNavigation": "Video Player Navigation", "videoPlayerNavigationDescription": "Use arrow keys to navigate video player controls", + "crashReporting": "Crash Reporting", + "crashReportingDescription": "Send crash reports to help improve the app", "debugLogging": "Debug Logging", "debugLoggingDescription": "Enable detailed logging for troubleshooting", "viewLogs": "View Logs", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index ad346a68..7d2de8af 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "Personalizar los atajos de teclado", "videoPlayerNavigation": "Navegación del Reproductor de Video", "videoPlayerNavigationDescription": "Usar las teclas de flecha para navegar por los controles del reproductor", + "crashReporting": "Informes de Errores", + "crashReportingDescription": "Enviar informes de errores para mejorar la aplicación", "debugLogging": "Registro de Depuración", "debugLoggingDescription": "Habilitar registros detallados para resolución de problemas", "viewLogs": "Ver Logs", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index a417243d..f0c12ed1 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "Personnaliser les raccourcis clavier", "videoPlayerNavigation": "Navigation dans le lecteur vidéo", "videoPlayerNavigationDescription": "Utilisez les touches fléchées pour naviguer dans les commandes du lecteur vidéo.", + "crashReporting": "Rapports de plantage", + "crashReportingDescription": "Envoyer des rapports de plantage pour améliorer l'application", "debugLogging": "Journalisation de débogage", "debugLoggingDescription": "Activer la journalisation détaillée pour le dépannage", "viewLogs": "Voir les logs", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 167aadc0..bbd3f052 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "Personalizza le scorciatoie da tastiera", "videoPlayerNavigation": "Navigazione del lettore video", "videoPlayerNavigationDescription": "Usa i tasti freccia per navigare nei controlli del lettore video", + "crashReporting": "Segnalazione errori", + "crashReportingDescription": "Invia segnalazioni di errori per migliorare l'app", "debugLogging": "Log di debug", "debugLoggingDescription": "Abilita il logging dettagliato per la risoluzione dei problemi", "viewLogs": "Visualizza log", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 143df37e..f2338f0e 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "사용자 정의 키보드 단축키", "videoPlayerNavigation": "비디오 플레이어 탐색", "videoPlayerNavigationDescription": "방향 키를 사용하여 비디오 플레이어 컨트롤 탐색", + "crashReporting": "충돌 보고", + "crashReportingDescription": "앱 개선을 위해 충돌 보고서 전송", "debugLogging": "디버그 로깅", "debugLoggingDescription": "문제 해결을 위해 상세 로깅 활성화", "viewLogs": "로그 보기", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 072182b0..4667a2db 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "Pas toetsenbord sneltoetsen aan", "videoPlayerNavigation": "Videospeler navigatie", "videoPlayerNavigationDescription": "Gebruik pijltjestoetsen om door de videospeler bediening te navigeren", + "crashReporting": "Crashrapportage", + "crashReportingDescription": "Crashrapporten verzenden om de app te verbeteren", "debugLogging": "Debug logging", "debugLoggingDescription": "Schakel gedetailleerde logging in voor probleemoplossing", "viewLogs": "Bekijk logs", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 2d2ff675..4af76497 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 9 -/// Strings: 7173 (797 per locale) +/// Strings: 7245 (805 per locale) /// -/// Built on 2026-02-24 at 21:50 UTC +/// Built on 2026-03-02 at 06:26 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index f7e47cb6..41c65712 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -270,6 +270,8 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn { @override String get keyboardShortcutsDescription => 'Tastenkürzel anpassen'; @override String get videoPlayerNavigation => 'Videoplayer-Navigation'; @override String get videoPlayerNavigationDescription => 'Pfeiltasten zur Navigation der Videoplayer-Steuerung verwenden'; + @override String get crashReporting => 'Absturzberichte'; + @override String get crashReportingDescription => 'Absturzberichte senden, um die App zu verbessern'; @override String get debugLogging => 'Debug-Protokollierung'; @override String get debugLoggingDescription => 'Detaillierte Protokolle zur Fehleranalyse aktivieren'; @override String get viewLogs => 'Protokolle anzeigen'; @@ -1441,6 +1443,8 @@ extension on TranslationsDe { 'settings.keyboardShortcutsDescription' => 'Tastenkürzel anpassen', 'settings.videoPlayerNavigation' => 'Videoplayer-Navigation', 'settings.videoPlayerNavigationDescription' => 'Pfeiltasten zur Navigation der Videoplayer-Steuerung verwenden', + 'settings.crashReporting' => 'Absturzberichte', + 'settings.crashReportingDescription' => 'Absturzberichte senden, um die App zu verbessern', 'settings.debugLogging' => 'Debug-Protokollierung', 'settings.debugLoggingDescription' => 'Detaillierte Protokolle zur Fehleranalyse aktivieren', 'settings.viewLogs' => 'Protokolle anzeigen', @@ -1487,6 +1491,8 @@ extension on TranslationsDe { 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.discordRichPresence' => 'Discord Rich Presence', 'settings.discordRichPresenceDescription' => 'Zeige auf Discord, was du gerade schaust', + 'settings.autoPip' => 'Automatisches Bild-in-Bild', + 'settings.autoPipDescription' => 'Automatisch Bild-in-Bild aktivieren, wenn die App während der Wiedergabe verlassen wird', 'settings.matchContentFrameRate' => 'Inhalts-Bildrate anpassen', 'settings.matchContentFrameRateDescription' => 'Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku', 'settings.tunneledPlayback' => 'Tunneled Playback', @@ -1808,6 +1814,8 @@ extension on TranslationsDe { 'liveTv.nowPlaying' => 'Läuft gerade', 'liveTv.noPrograms' => 'Keine Programmdaten verfügbar', 'liveTv.channelNumber' => ({required Object number}) => 'Kanal ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => 'LIVE', 'liveTv.hd' => 'HD', 'liveTv.premiere' => 'NEU', @@ -1822,8 +1830,6 @@ extension on TranslationsDe { 'liveTv.evening' => 'Abend', 'liveTv.lateNight' => 'Spätnacht', 'liveTv.whatsOn' => 'Jetzt im TV', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => 'Kanal ansehen', 'downloads.title' => 'Downloads', 'downloads.manage' => 'Verwalten', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 98b64839..f45aa35c 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -472,7 +472,7 @@ class TranslationsSettingsEn { /// en: 'Hide Spoilers for Unwatched Episodes' String get hideSpoilers => 'Hide Spoilers for Unwatched Episodes'; - /// en: 'Blur thumbnails and hide descriptions for episodes you haven\'t watched yet' + /// en: 'Blur thumbnails and hide descriptions for episodes you haven't watched yet' String get hideSpoilersDescription => 'Blur thumbnails and hide descriptions for episodes you haven\'t watched yet'; /// en: 'Player Backend' @@ -556,6 +556,12 @@ class TranslationsSettingsEn { /// en: 'Use arrow keys to navigate video player controls' String get videoPlayerNavigationDescription => 'Use arrow keys to navigate video player controls'; + /// en: 'Crash Reporting' + String get crashReporting => 'Crash Reporting'; + + /// en: 'Send crash reports to help improve the app' + String get crashReportingDescription => 'Send crash reports to help improve the app'; + /// en: 'Debug Logging' String get debugLogging => 'Debug Logging'; @@ -2807,7 +2813,6 @@ class TranslationsCompanionRemotePairingEn { /// en: 'Failed to connect: ${error}' String failedToConnect({required Object error}) => 'Failed to connect: ${error}'; - } // Path: companionRemote.remote @@ -3055,6 +3060,8 @@ extension on Translations { 'settings.keyboardShortcutsDescription' => 'Customize keyboard shortcuts', 'settings.videoPlayerNavigation' => 'Video Player Navigation', 'settings.videoPlayerNavigationDescription' => 'Use arrow keys to navigate video player controls', + 'settings.crashReporting' => 'Crash Reporting', + 'settings.crashReportingDescription' => 'Send crash reports to help improve the app', 'settings.debugLogging' => 'Debug Logging', 'settings.debugLoggingDescription' => 'Enable detailed logging for troubleshooting', 'settings.viewLogs' => 'View Logs', @@ -3424,6 +3431,8 @@ extension on Translations { 'liveTv.nowPlaying' => 'Now Playing', 'liveTv.noPrograms' => 'No program data available', 'liveTv.channelNumber' => ({required Object number}) => 'Ch. ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => 'LIVE', 'liveTv.hd' => 'HD', 'liveTv.premiere' => 'NEW', @@ -3438,8 +3447,6 @@ extension on Translations { 'liveTv.evening' => 'Evening', 'liveTv.lateNight' => 'Late Night', 'liveTv.whatsOn' => 'What\'s On', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => 'Watch Channel', 'collections.title' => 'Collections', 'collections.collection' => 'Collection', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 31e81bd2..65e21fab 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -270,6 +270,8 @@ class _TranslationsSettingsEs implements TranslationsSettingsEn { @override String get keyboardShortcutsDescription => 'Personalizar los atajos de teclado'; @override String get videoPlayerNavigation => 'Navegación del Reproductor de Video'; @override String get videoPlayerNavigationDescription => 'Usar las teclas de flecha para navegar por los controles del reproductor'; + @override String get crashReporting => 'Informes de Errores'; + @override String get crashReportingDescription => 'Enviar informes de errores para mejorar la aplicación'; @override String get debugLogging => 'Registro de Depuración'; @override String get debugLoggingDescription => 'Habilitar registros detallados para resolución de problemas'; @override String get viewLogs => 'Ver Logs'; @@ -1441,6 +1443,8 @@ extension on TranslationsEs { 'settings.keyboardShortcutsDescription' => 'Personalizar los atajos de teclado', 'settings.videoPlayerNavigation' => 'Navegación del Reproductor de Video', 'settings.videoPlayerNavigationDescription' => 'Usar las teclas de flecha para navegar por los controles del reproductor', + 'settings.crashReporting' => 'Informes de Errores', + 'settings.crashReportingDescription' => 'Enviar informes de errores para mejorar la aplicación', 'settings.debugLogging' => 'Registro de Depuración', 'settings.debugLoggingDescription' => 'Habilitar registros detallados para resolución de problemas', 'settings.viewLogs' => 'Ver Logs', @@ -1487,6 +1491,8 @@ extension on TranslationsEs { 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.discordRichPresence' => 'Presencia de Discord', 'settings.discordRichPresenceDescription' => 'Mostrar lo que estás viendo en Discord', + 'settings.autoPip' => 'Imagen en imagen automática', + 'settings.autoPipDescription' => 'Activar automáticamente imagen en imagen al salir de la app durante la reproducción', 'settings.matchContentFrameRate' => 'Ajustar frecuencia de actualización', 'settings.matchContentFrameRateDescription' => 'Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería', 'settings.tunneledPlayback' => 'Tunneled Playback', @@ -1808,6 +1814,8 @@ extension on TranslationsEs { 'liveTv.nowPlaying' => 'Reproduciendo ahora', 'liveTv.noPrograms' => 'No hay datos de programación disponibles', 'liveTv.channelNumber' => ({required Object number}) => 'Canal ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => 'EN VIVO', 'liveTv.hd' => 'HD', 'liveTv.premiere' => 'NUEVO', @@ -1822,8 +1830,6 @@ extension on TranslationsEs { 'liveTv.evening' => 'Noche', 'liveTv.lateNight' => 'Trasnoche', 'liveTv.whatsOn' => 'En emisión', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => 'Ver canal', 'collections.title' => 'Colecciones', 'collections.collection' => 'Colección', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index bd90314c..8607cad1 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -270,6 +270,8 @@ class _TranslationsSettingsFr implements TranslationsSettingsEn { @override String get keyboardShortcutsDescription => 'Personnaliser les raccourcis clavier'; @override String get videoPlayerNavigation => 'Navigation dans le lecteur vidéo'; @override String get videoPlayerNavigationDescription => 'Utilisez les touches fléchées pour naviguer dans les commandes du lecteur vidéo.'; + @override String get crashReporting => 'Rapports de plantage'; + @override String get crashReportingDescription => 'Envoyer des rapports de plantage pour améliorer l\'application'; @override String get debugLogging => 'Journalisation de débogage'; @override String get debugLoggingDescription => 'Activer la journalisation détaillée pour le dépannage'; @override String get viewLogs => 'Voir les logs'; @@ -1441,6 +1443,8 @@ extension on TranslationsFr { 'settings.keyboardShortcutsDescription' => 'Personnaliser les raccourcis clavier', 'settings.videoPlayerNavigation' => 'Navigation dans le lecteur vidéo', 'settings.videoPlayerNavigationDescription' => 'Utilisez les touches fléchées pour naviguer dans les commandes du lecteur vidéo.', + 'settings.crashReporting' => 'Rapports de plantage', + 'settings.crashReportingDescription' => 'Envoyer des rapports de plantage pour améliorer l\'application', 'settings.debugLogging' => 'Journalisation de débogage', 'settings.debugLoggingDescription' => 'Activer la journalisation détaillée pour le dépannage', 'settings.viewLogs' => 'Voir les logs', @@ -1487,6 +1491,8 @@ extension on TranslationsFr { 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.discordRichPresence' => 'Discord Rich Presence', 'settings.discordRichPresenceDescription' => 'Montrez ce que vous regardez sur Discord', + 'settings.autoPip' => 'Image dans l\'image automatique', + 'settings.autoPipDescription' => 'Activer automatiquement l\'image dans l\'image en quittant l\'application pendant la lecture', 'settings.matchContentFrameRate' => 'Fréquence d\'images du contenu correspondant', 'settings.matchContentFrameRateDescription' => 'Ajustez la fréquence de rafraîchissement de l\'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie', 'settings.tunneledPlayback' => 'Tunneled Playback', @@ -1808,6 +1814,8 @@ extension on TranslationsFr { 'liveTv.nowPlaying' => 'En cours de lecture', 'liveTv.noPrograms' => 'Aucune donnée de programme disponible', 'liveTv.channelNumber' => ({required Object number}) => 'Ch. ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => 'EN DIRECT', 'liveTv.hd' => 'HD', 'liveTv.premiere' => 'NOUVEAU', @@ -1822,8 +1830,6 @@ extension on TranslationsFr { 'liveTv.evening' => 'Soirée', 'liveTv.lateNight' => 'Nuit tardive', 'liveTv.whatsOn' => 'En ce moment', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => 'Regarder la chaîne', 'collections.title' => 'Collections', 'collections.collection' => 'Collection', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 073bee03..e7ec180e 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -270,6 +270,8 @@ class _TranslationsSettingsIt implements TranslationsSettingsEn { @override String get keyboardShortcutsDescription => 'Personalizza le scorciatoie da tastiera'; @override String get videoPlayerNavigation => 'Navigazione del lettore video'; @override String get videoPlayerNavigationDescription => 'Usa i tasti freccia per navigare nei controlli del lettore video'; + @override String get crashReporting => 'Segnalazione errori'; + @override String get crashReportingDescription => 'Invia segnalazioni di errori per migliorare l\'app'; @override String get debugLogging => 'Log di debug'; @override String get debugLoggingDescription => 'Abilita il logging dettagliato per la risoluzione dei problemi'; @override String get viewLogs => 'Visualizza log'; @@ -1441,6 +1443,8 @@ extension on TranslationsIt { 'settings.keyboardShortcutsDescription' => 'Personalizza le scorciatoie da tastiera', 'settings.videoPlayerNavigation' => 'Navigazione del lettore video', 'settings.videoPlayerNavigationDescription' => 'Usa i tasti freccia per navigare nei controlli del lettore video', + 'settings.crashReporting' => 'Segnalazione errori', + 'settings.crashReportingDescription' => 'Invia segnalazioni di errori per migliorare l\'app', 'settings.debugLogging' => 'Log di debug', 'settings.debugLoggingDescription' => 'Abilita il logging dettagliato per la risoluzione dei problemi', 'settings.viewLogs' => 'Visualizza log', @@ -1487,6 +1491,8 @@ extension on TranslationsIt { 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.discordRichPresence' => 'Discord Rich Presence', 'settings.discordRichPresenceDescription' => 'Mostra su Discord cosa stai guardando', + 'settings.autoPip' => 'Picture-in-Picture automatico', + 'settings.autoPipDescription' => 'Attiva automaticamente il picture-in-picture quando si esce dall\'app durante la riproduzione', 'settings.matchContentFrameRate' => 'Adatta frequenza fotogrammi', 'settings.matchContentFrameRateDescription' => 'Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria', 'settings.tunneledPlayback' => 'Tunneled Playback', @@ -1808,6 +1814,8 @@ extension on TranslationsIt { 'liveTv.nowPlaying' => 'In riproduzione', 'liveTv.noPrograms' => 'Nessun dato di programma disponibile', 'liveTv.channelNumber' => ({required Object number}) => 'Canale ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => 'IN DIRETTA', 'liveTv.hd' => 'HD', 'liveTv.premiere' => 'NUOVO', @@ -1822,8 +1830,6 @@ extension on TranslationsIt { 'liveTv.evening' => 'Sera', 'liveTv.lateNight' => 'Notte tarda', 'liveTv.whatsOn' => 'In onda ora', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => 'Guarda canale', 'downloads.title' => 'Download', 'downloads.manage' => 'Gestisci', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index d014707e..4fd541bc 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -270,6 +270,8 @@ class _TranslationsSettingsKo implements TranslationsSettingsEn { @override String get keyboardShortcutsDescription => '사용자 정의 키보드 단축키'; @override String get videoPlayerNavigation => '비디오 플레이어 탐색'; @override String get videoPlayerNavigationDescription => '방향 키를 사용하여 비디오 플레이어 컨트롤 탐색'; + @override String get crashReporting => '충돌 보고'; + @override String get crashReportingDescription => '앱 개선을 위해 충돌 보고서 전송'; @override String get debugLogging => '디버그 로깅'; @override String get debugLoggingDescription => '문제 해결을 위해 상세 로깅 활성화'; @override String get viewLogs => '로그 보기'; @@ -1441,6 +1443,8 @@ extension on TranslationsKo { 'settings.keyboardShortcutsDescription' => '사용자 정의 키보드 단축키', 'settings.videoPlayerNavigation' => '비디오 플레이어 탐색', 'settings.videoPlayerNavigationDescription' => '방향 키를 사용하여 비디오 플레이어 컨트롤 탐색', + 'settings.crashReporting' => '충돌 보고', + 'settings.crashReportingDescription' => '앱 개선을 위해 충돌 보고서 전송', 'settings.debugLogging' => '디버그 로깅', 'settings.debugLoggingDescription' => '문제 해결을 위해 상세 로깅 활성화', 'settings.viewLogs' => '로그 보기', @@ -1487,6 +1491,8 @@ extension on TranslationsKo { 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.discordRichPresence' => 'Discord Rich Presence', 'settings.discordRichPresenceDescription' => 'Discord에서 시청 중인 콘텐츠 표시', + 'settings.autoPip' => '자동 PIP 모드', + 'settings.autoPipDescription' => '재생 중 앱을 나갈 때 자동으로 PIP 모드로 전환', 'settings.matchContentFrameRate' => '콘텐츠 프레임 레이트 맞춤', 'settings.matchContentFrameRateDescription' => '비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다', 'settings.tunneledPlayback' => 'Tunneled Playback', @@ -1808,6 +1814,8 @@ extension on TranslationsKo { 'liveTv.nowPlaying' => '현재 재생 중', 'liveTv.noPrograms' => '프로그램 데이터가 없습니다', 'liveTv.channelNumber' => ({required Object number}) => '채널 ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => '실시간', 'liveTv.hd' => 'HD', 'liveTv.premiere' => '신규', @@ -1822,8 +1830,6 @@ extension on TranslationsKo { 'liveTv.evening' => '저녁', 'liveTv.lateNight' => '심야 방송', 'liveTv.whatsOn' => '지금 방송 중', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => '채널 시청', 'collections.title' => '컬렉션', 'collections.collection' => '컬렉션', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 77865c8c..1d03c4e3 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -270,6 +270,8 @@ class _TranslationsSettingsNl implements TranslationsSettingsEn { @override String get keyboardShortcutsDescription => 'Pas toetsenbord sneltoetsen aan'; @override String get videoPlayerNavigation => 'Videospeler navigatie'; @override String get videoPlayerNavigationDescription => 'Gebruik pijltjestoetsen om door de videospeler bediening te navigeren'; + @override String get crashReporting => 'Crashrapportage'; + @override String get crashReportingDescription => 'Crashrapporten verzenden om de app te verbeteren'; @override String get debugLogging => 'Debug logging'; @override String get debugLoggingDescription => 'Schakel gedetailleerde logging in voor probleemoplossing'; @override String get viewLogs => 'Bekijk logs'; @@ -1441,6 +1443,8 @@ extension on TranslationsNl { 'settings.keyboardShortcutsDescription' => 'Pas toetsenbord sneltoetsen aan', 'settings.videoPlayerNavigation' => 'Videospeler navigatie', 'settings.videoPlayerNavigationDescription' => 'Gebruik pijltjestoetsen om door de videospeler bediening te navigeren', + 'settings.crashReporting' => 'Crashrapportage', + 'settings.crashReportingDescription' => 'Crashrapporten verzenden om de app te verbeteren', 'settings.debugLogging' => 'Debug logging', 'settings.debugLoggingDescription' => 'Schakel gedetailleerde logging in voor probleemoplossing', 'settings.viewLogs' => 'Bekijk logs', @@ -1487,6 +1491,8 @@ extension on TranslationsNl { 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.discordRichPresence' => 'Discord Rich Presence', 'settings.discordRichPresenceDescription' => 'Toon op Discord wat je aan het kijken bent', + 'settings.autoPip' => 'Automatische beeld-in-beeld', + 'settings.autoPipDescription' => 'Automatisch beeld-in-beeld activeren bij het verlaten van de app tijdens afspelen', 'settings.matchContentFrameRate' => 'Inhoudsframesnelheid afstemmen', 'settings.matchContentFrameRateDescription' => 'Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij', 'settings.tunneledPlayback' => 'Tunneled Playback', @@ -1808,6 +1814,8 @@ extension on TranslationsNl { 'liveTv.nowPlaying' => 'Nu aan het afspelen', 'liveTv.noPrograms' => 'Geen programmagegevens beschikbaar', 'liveTv.channelNumber' => ({required Object number}) => 'Kanaal ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => 'LIVE', 'liveTv.hd' => 'HD', 'liveTv.premiere' => 'NIEUW', @@ -1822,8 +1830,6 @@ extension on TranslationsNl { 'liveTv.evening' => 'Avond', 'liveTv.lateNight' => 'Late avond', 'liveTv.whatsOn' => 'Nu op TV', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => 'Kanaal bekijken', 'downloads.title' => 'Downloads', 'downloads.manage' => 'Beheren', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 1b90d63f..41325bf6 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -270,6 +270,8 @@ class _TranslationsSettingsSv implements TranslationsSettingsEn { @override String get keyboardShortcutsDescription => 'Anpassa tangentbordsgenvägar'; @override String get videoPlayerNavigation => 'Navigering i videospelaren'; @override String get videoPlayerNavigationDescription => 'Använd piltangenter för att navigera videospelarens kontroller'; + @override String get crashReporting => 'Kraschrapportering'; + @override String get crashReportingDescription => 'Skicka kraschrapporter för att förbättra appen'; @override String get debugLogging => 'Felsökningsloggning'; @override String get debugLoggingDescription => 'Aktivera detaljerad loggning för felsökning'; @override String get viewLogs => 'Visa loggar'; @@ -1441,6 +1443,8 @@ extension on TranslationsSv { 'settings.keyboardShortcutsDescription' => 'Anpassa tangentbordsgenvägar', 'settings.videoPlayerNavigation' => 'Navigering i videospelaren', 'settings.videoPlayerNavigationDescription' => 'Använd piltangenter för att navigera videospelarens kontroller', + 'settings.crashReporting' => 'Kraschrapportering', + 'settings.crashReportingDescription' => 'Skicka kraschrapporter för att förbättra appen', 'settings.debugLogging' => 'Felsökningsloggning', 'settings.debugLoggingDescription' => 'Aktivera detaljerad loggning för felsökning', 'settings.viewLogs' => 'Visa loggar', @@ -1487,6 +1491,8 @@ extension on TranslationsSv { 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.discordRichPresence' => 'Discord Rich Presence', 'settings.discordRichPresenceDescription' => 'Visa vad du tittar på i Discord', + 'settings.autoPip' => 'Automatisk bild-i-bild', + 'settings.autoPipDescription' => 'Aktivera bild-i-bild automatiskt när appen lämnas under uppspelning', 'settings.matchContentFrameRate' => 'Matcha innehållets bildfrekvens', 'settings.matchContentFrameRateDescription' => 'Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri', 'settings.tunneledPlayback' => 'Tunneled Playback', @@ -1808,6 +1814,8 @@ extension on TranslationsSv { 'liveTv.nowPlaying' => 'Spelas nu', 'liveTv.noPrograms' => 'Ingen programdata tillgänglig', 'liveTv.channelNumber' => ({required Object number}) => 'Kanal ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => 'LIVE', 'liveTv.hd' => 'HD', 'liveTv.premiere' => 'NY', @@ -1822,8 +1830,6 @@ extension on TranslationsSv { 'liveTv.evening' => 'Kväll', 'liveTv.lateNight' => 'Sen kväll', 'liveTv.whatsOn' => 'På TV nu', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => 'Titta på kanal', 'downloads.title' => 'Nedladdningar', 'downloads.manage' => 'Hantera', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 7b4e22bd..260fd35f 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -270,6 +270,8 @@ class _TranslationsSettingsZh implements TranslationsSettingsEn { @override String get keyboardShortcutsDescription => '自定义键盘快捷键'; @override String get videoPlayerNavigation => '视频播放器导航'; @override String get videoPlayerNavigationDescription => '使用方向键导航视频播放器控件'; + @override String get crashReporting => '崩溃报告'; + @override String get crashReportingDescription => '发送崩溃报告以帮助改进应用'; @override String get debugLogging => '调试日志'; @override String get debugLoggingDescription => '启用详细日志记录以便故障排除'; @override String get viewLogs => '查看日志'; @@ -1441,6 +1443,8 @@ extension on TranslationsZh { 'settings.keyboardShortcutsDescription' => '自定义键盘快捷键', 'settings.videoPlayerNavigation' => '视频播放器导航', 'settings.videoPlayerNavigationDescription' => '使用方向键导航视频播放器控件', + 'settings.crashReporting' => '崩溃报告', + 'settings.crashReportingDescription' => '发送崩溃报告以帮助改进应用', 'settings.debugLogging' => '调试日志', 'settings.debugLoggingDescription' => '启用详细日志记录以便故障排除', 'settings.viewLogs' => '查看日志', @@ -1487,6 +1491,8 @@ extension on TranslationsZh { 'settings.maxVolumePercent' => ({required Object percent}) => '${percent}%', 'settings.discordRichPresence' => 'Discord 动态状态', 'settings.discordRichPresenceDescription' => '在 Discord 上显示您正在观看的内容', + 'settings.autoPip' => '自动画中画', + 'settings.autoPipDescription' => '在播放期间离开应用时自动进入画中画模式', 'settings.matchContentFrameRate' => '匹配内容帧率', 'settings.matchContentFrameRateDescription' => '调整显示刷新率以匹配视频内容,减少画面抖动并节省电量', 'settings.tunneledPlayback' => 'Tunneled Playback', @@ -1808,6 +1814,8 @@ extension on TranslationsZh { 'liveTv.nowPlaying' => '正在播放', 'liveTv.noPrograms' => '没有可用的节目数据', 'liveTv.channelNumber' => ({required Object number}) => '频道 ${number}', + _ => null, + } ?? switch (path) { 'liveTv.live' => '直播', 'liveTv.hd' => '高清', 'liveTv.premiere' => '新', @@ -1822,8 +1830,6 @@ extension on TranslationsZh { 'liveTv.evening' => '晚上', 'liveTv.lateNight' => '深夜', 'liveTv.whatsOn' => '正在播出', - _ => null, - } ?? switch (path) { 'liveTv.watchChannel' => '观看频道', 'downloads.title' => '下载', 'downloads.manage' => '管理', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 151b5c20..197c63c5 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "Anpassa tangentbordsgenvägar", "videoPlayerNavigation": "Navigering i videospelaren", "videoPlayerNavigationDescription": "Använd piltangenter för att navigera videospelarens kontroller", + "crashReporting": "Kraschrapportering", + "crashReportingDescription": "Skicka kraschrapporter för att förbättra appen", "debugLogging": "Felsökningsloggning", "debugLoggingDescription": "Aktivera detaljerad loggning för felsökning", "viewLogs": "Visa loggar", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index ff6687e8..00967371 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -151,6 +151,8 @@ "keyboardShortcutsDescription": "自定义键盘快捷键", "videoPlayerNavigation": "视频播放器导航", "videoPlayerNavigationDescription": "使用方向键导航视频播放器控件", + "crashReporting": "崩溃报告", + "crashReportingDescription": "发送崩溃报告以帮助改进应用", "debugLogging": "调试日志", "debugLoggingDescription": "启用详细日志记录以便故障排除", "viewLogs": "查看日志", diff --git a/lib/main.dart b/lib/main.dart index 8dd3fb54..d574ff8f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; @@ -5,6 +7,7 @@ import 'dart:io' show Platform; import 'package:window_manager/window_manager.dart'; import 'package:provider/provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'screens/main_screen.dart'; import 'screens/auth_screen.dart'; import 'services/storage_service.dart'; @@ -48,6 +51,7 @@ import 'focus/input_mode_tracker.dart'; import 'focus/key_event_utils.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'utils/navigation_transitions.dart'; +import 'utils/log_redaction_manager.dart'; // Workaround for Flutter bug #177992: iPadOS 26.1+ misinterprets fake touch events // at (0,0) as barrier taps, causing modals to dismiss immediately. @@ -70,69 +74,115 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); _installZeroOffsetPointerGuard(); // Workaround for iPadOS 26.1+ modal dismissal bug - // Initialize settings first to get saved locale - final settings = await SettingsService.getInstance(); - final savedLocale = settings.getAppLocale(); + await SentryFlutter.init( + (options) { + options.dsn = 'https://7aa61595518d4770a4d5ea412982dcd7@bugs.plezy.app/1'; + options.sendDefaultPii = true; + options.tracesSampleRate = 0; + options.anrEnabled = true; + options.anrTimeoutInterval = const Duration(seconds: 5); + options.attachStacktrace = true; + options.beforeSend = _beforeSend; + }, + appRunner: () async { + // Initialize settings first to get saved locale + final settings = await SettingsService.getInstance(); + final savedLocale = settings.getAppLocale(); - // Initialize localization with saved locale - LocaleSettings.setLocale(savedLocale); + // Initialize localization with saved locale + LocaleSettings.setLocale(savedLocale); - // Needed for formatting dates in different locales - await initializeDateFormatting(savedLocale.languageCode, null); + // Needed for formatting dates in different locales + await initializeDateFormatting(savedLocale.languageCode, null); - // Configure image cache for large libraries - PaintingBinding.instance.imageCache.maximumSize = 2000; // default 1000 - PaintingBinding.instance.imageCache.maximumSizeBytes = 300 << 20; // 300MB + // Configure image cache for large libraries + PaintingBinding.instance.imageCache.maximumSize = 2000; // default 1000 + PaintingBinding.instance.imageCache.maximumSizeBytes = 300 << 20; // 300MB - // Initialize services in parallel where possible - final futures = >[]; + // Initialize services in parallel where possible + final futures = >[]; - // Initialize window_manager for desktop platforms - if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { - futures.add(windowManager.ensureInitialized()); + // Initialize window_manager for desktop platforms + if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { + futures.add(windowManager.ensureInitialized()); + } + + // Initialize TV detection and PiP service for Android + if (Platform.isAndroid) { + futures.add(TvDetectionService.getInstance()); + // Initialize PiP service to listen for PiP state changes + PipService(); + } + + // Configure macOS window with custom titlebar (depends on window manager) + futures.add(MacOSWindowService.setupCustomTitlebar()); + + // Initialize storage service + futures.add(StorageService.getInstance()); + + // Initialize language codes for track selection + futures.add(LanguageCodes.initialize()); + + // Wait for all parallel services to complete + await Future.wait(futures); + + // Initialize logger level based on debug setting + final debugEnabled = settings.getEnableDebugLogging(); + setLoggerLevel(debugEnabled); + + // Initialize download storage service with settings + await DownloadStorageService.instance.initialize(settings); + + // Start global fullscreen state monitoring + FullscreenStateManager().startMonitoring(); + + // Initialize gamepad service for desktop platforms + if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { + GamepadService.instance.start(); + DiscordRPCService.instance.initialize(); + } + + // DTD service is available for MCP tooling connection if needed + + // Register bundled shader licenses + _registerShaderLicenses(); + + runApp(const MainApp()); + }, + ); +} + +FutureOr _beforeSend(SentryEvent event, Hint hint) { + // Drop event if user opted out of crash reporting + final instance = SettingsService.instanceOrNull; + if (instance != null && !instance.getCrashReporting()) return null; + + // Scrub Plex tokens and server URLs from exception messages + var exceptions = event.exceptions; + if (exceptions != null) { + exceptions = exceptions.map((e) { + final value = e.value; + if (value != null) { + return e.copyWith(value: LogRedactionManager.redact(value)); + } + return e; + }).toList(); } - // Initialize TV detection and PiP service for Android - if (Platform.isAndroid) { - futures.add(TvDetectionService.getInstance()); - // Initialize PiP service to listen for PiP state changes - PipService(); + // Scrub breadcrumb messages and data + var breadcrumbs = event.breadcrumbs; + if (breadcrumbs != null) { + breadcrumbs = breadcrumbs.map((b) { + final message = b.message; + final data = b.data; + return b.copyWith( + message: message != null ? LogRedactionManager.redact(message) : null, + data: data?.map((k, v) => MapEntry(k, v is String ? LogRedactionManager.redact(v) : v)), + ); + }).toList(); } - // Configure macOS window with custom titlebar (depends on window manager) - futures.add(MacOSWindowService.setupCustomTitlebar()); - - // Initialize storage service - futures.add(StorageService.getInstance()); - - // Initialize language codes for track selection - futures.add(LanguageCodes.initialize()); - - // Wait for all parallel services to complete - await Future.wait(futures); - - // Initialize logger level based on debug setting - final debugEnabled = settings.getEnableDebugLogging(); - setLoggerLevel(debugEnabled); - - // Initialize download storage service with settings - await DownloadStorageService.instance.initialize(settings); - - // Start global fullscreen state monitoring - FullscreenStateManager().startMonitoring(); - - // Initialize gamepad service for desktop platforms - if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { - GamepadService.instance.start(); - DiscordRPCService.instance.initialize(); - } - - // DTD service is available for MCP tooling connection if needed - - // Register bundled shader licenses - _registerShaderLicenses(); - - runApp(const MainApp()); + return event.copyWith(exceptions: exceptions, breadcrumbs: breadcrumbs); } void _registerShaderLicenses() { diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 992bfa3d..1b5d3732 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../models/hotkey_model.dart'; @@ -97,6 +99,7 @@ class _SettingsScreenState extends State with FocusableTab { static const _kDownloadOnWifiOnly = 'download_on_wifi_only'; static const _kVideoPlayerControls = 'video_player_controls'; static const _kVideoPlayerNavigation = 'video_player_navigation'; + static const _kCrashReporting = 'crash_reporting'; static const _kDebugLogging = 'debug_logging'; static const _kViewLogs = 'view_logs'; static const _kClearCache = 'clear_cache'; @@ -107,6 +110,7 @@ class _SettingsScreenState extends State with FocusableTab { late final bool _keyboardShortcutsSupported = KeyboardShortcutsService.isPlatformSupported(); bool _isLoading = true; + bool _crashReporting = true; bool _enableDebugLogging = false; bool _enableHardwareDecoding = true; int _bufferSize = 0; @@ -183,6 +187,7 @@ class _SettingsScreenState extends State with FocusableTab { if (!mounted) return; setState(() { + _crashReporting = _settingsService.getCrashReporting(); _enableDebugLogging = _settingsService.getEnableDebugLogging(); _enableHardwareDecoding = _settingsService.getEnableHardwareDecoding(); _bufferSize = _settingsService.getBufferSize(); @@ -490,7 +495,7 @@ class _SettingsScreenState extends State with FocusableTab { await _settingsService.setEnableHardwareDecoding(value); }, ), - if (Platform.isAndroid) + if (Platform.isAndroid || Platform.isIOS) SwitchListTile( focusNode: _focusTracker.get(_kAutoPip), secondary: const AppIcon(Symbols.picture_in_picture_alt_rounded, fill: 1), @@ -935,6 +940,19 @@ class _SettingsScreenState extends State with FocusableTab { style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), + SwitchListTile( + focusNode: _focusTracker.get(_kCrashReporting), + secondary: const AppIcon(Symbols.monitoring_rounded, fill: 1), + title: Text(t.settings.crashReporting), + subtitle: Text(t.settings.crashReportingDescription), + value: _crashReporting, + onChanged: (value) async { + setState(() { + _crashReporting = value; + }); + await _settingsService.setCrashReporting(value); + }, + ), SwitchListTile( focusNode: _focusTracker.get(_kDebugLogging), secondary: const AppIcon(Symbols.bug_report_rounded, fill: 1), @@ -974,6 +992,17 @@ class _SettingsScreenState extends State with FocusableTab { trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), onTap: () => _showResetSettingsDialog(), ), + if (kDebugMode) + ListTile( + leading: const AppIcon(Symbols.error_rounded, fill: 1), + title: const Text('Test Sentry'), + subtitle: const Text('Send a test error to Bugsink'), + trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), + onTap: () { + Sentry.captureException(Exception('Sentry test from settings')); + showSnackBar(context, 'Test error sent to Sentry'); + }, + ), ], ), ); diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 01516cf3..2980364f 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -8,6 +8,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:flutter/services.dart'; import 'package:os_media_controls/os_media_controls.dart'; import 'package:provider/provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; @@ -581,6 +582,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin _playbackRestartSubscription = player!.streams.playbackRestart.listen((_) async { if (!_hasFirstFrame.value) { _hasFirstFrame.value = true; + Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player')); // Apply frame rate matching on Android if enabled if (Platform.isAndroid && settingsService.getMatchContentFrameRate()) { @@ -648,6 +650,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Set MPV video-sync mode for smoother playback when display is synced await player!.setProperty('video-sync', 'display-tempo'); + Sentry.addBreadcrumb(Breadcrumb(message: 'Frame rate matching: ${fps}fps', category: 'player')); appLogger.d('Frame rate matching: Set display to ${fps}fps (duration: ${durationMs}ms)'); } catch (e) { appLogger.w('Failed to apply frame rate matching', error: e); @@ -661,6 +664,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin try { await player!.clearVideoFrameRate(); await player!.setProperty('video-sync', 'audio'); + Sentry.addBreadcrumb(Breadcrumb(message: 'Frame rate matching cleared', category: 'player')); appLogger.d('Frame rate matching: Cleared, restored default display mode'); } catch (e) { appLogger.d('Failed to clear frame rate matching', error: e); @@ -1764,6 +1768,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin } } + Sentry.addBreadcrumb(Breadcrumb(message: 'Player dispose', category: 'player')); player?.dispose(); if (_activeRatingKey == widget.metadata.ratingKey) { _activeRatingKey = null; diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 1bde1a1b..ce504354 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -19,6 +19,7 @@ enum EpisodePosterMode { seriesPoster, seasonPoster, episodeThumbnail } class SettingsService extends BaseSharedPreferencesService { static const String _keyThemeMode = 'theme_mode'; static const String _keyEnableDebugLogging = 'enable_debug_logging'; + static const String _keyCrashReporting = 'crash_reporting'; static const String _keyBufferSize = 'buffer_size'; static const String _keyBufferSizeMigratedToAuto = 'buffer_size_migrated_to_auto'; static const String _keyKeyboardShortcuts = 'keyboard_shortcuts'; @@ -85,10 +86,16 @@ class SettingsService extends BaseSharedPreferencesService { SettingsService._(); - static Future getInstance() { - return BaseSharedPreferencesService.initializeInstance(() => SettingsService._()); + static SettingsService? _cachedInstance; + + static Future getInstance() async { + _cachedInstance ??= await BaseSharedPreferencesService.initializeInstance(() => SettingsService._()); + return _cachedInstance!; } + /// Synchronous access to the singleton, or null if not yet initialized. + static SettingsService? get instanceOrNull => _cachedInstance; + /// Generic helper to get an enum value from preferences T _getEnumValue(String key, List values, T defaultValue) { final stored = prefs.getString(key); @@ -121,6 +128,15 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getBool(_keyEnableDebugLogging) ?? false; } + // Crash Reporting + Future setCrashReporting(bool enabled) async { + await prefs.setBool(_keyCrashReporting, enabled); + } + + bool getCrashReporting() { + return prefs.getBool(_keyCrashReporting) ?? true; // Default enabled + } + // Buffer Size (in MB) Future setBufferSize(int sizeInMB) async { await prefs.setInt(_keyBufferSize, sizeInMB); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 0a25aa35..99f59b60 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); + g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); + sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index eb699977..90734f69 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST os_media_controls screen_retriever_linux + sentry_flutter sqlite3_flutter_libs universal_gamepad url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a9d35d16..b7d119be 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,6 +15,7 @@ import os_media_controls import package_info_plus import path_provider_foundation import screen_retriever_macos +import sentry_flutter import shared_preferences_foundation import sqflite_darwin import sqlite3_flutter_libs @@ -34,6 +35,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 650efab9..a68dc837 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -22,6 +22,11 @@ PODS: - FlutterMacOS - screen_retriever_macos (0.0.1): - FlutterMacOS + - Sentry/HybridSDK (8.46.0) + - sentry_flutter (8.14.2): + - Flutter + - FlutterMacOS + - Sentry/HybridSDK (= 8.46.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -75,6 +80,7 @@ DEPENDENCIES: - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) + - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) @@ -85,6 +91,7 @@ DEPENDENCIES: SPEC REPOS: trunk: + - Sentry - Sparkle - sqlite3 @@ -111,6 +118,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin screen_retriever_macos: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos + sentry_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite_darwin: @@ -138,6 +147,8 @@ SPEC CHECKSUMS: package_info_plus: f0052d280d17aa382b932f399edf32507174e870 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 + sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb Sparkle: f4355f9ebbe9b7d932df4980d70f13922ac97b2a sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 diff --git a/pubspec.lock b/pubspec.lock index bd16bc25..74448f61 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1008,6 +1008,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + sentry: + dependency: transitive + description: + name: sentry + sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb" + url: "https://pub.dev" + source: hosted + version: "8.14.2" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: "5ba2cf40646a77d113b37a07bd69f61bb3ec8a73cbabe5537b05a7c89d2656f8" + url: "https://pub.dev" + source: hosted + version: "8.14.2" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2e7974a8..76d10db6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: mobile_scanner: ^6.0.2 android_intent_plus: ^5.0.2 background_downloader: ^9.5.2 + sentry_flutter: ^8.12.0 auto_updater: git: url: https://github.com/edde746/auto_updater diff --git a/server/Caddyfile b/server/Caddyfile new file mode 100644 index 00000000..99b816d8 --- /dev/null +++ b/server/Caddyfile @@ -0,0 +1,9 @@ +ice.plezy.app { + reverse_proxy relay:8080 +} + +bugs.plezy.app { + reverse_proxy bugsink:8000 { + header_up X-Real-IP {remote_host} + } +} diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 00000000..1e09cc01 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.22-alpine AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /relay . + +FROM scratch +COPY --from=build /relay /relay +VOLUME /data/logs +EXPOSE 8080 +ENV GOMEMLIMIT=64MiB +ENTRYPOINT ["/relay"] diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 00000000..dbb97408 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,36 @@ +services: + relay: + build: . + restart: unless-stopped + volumes: + - logs:/data/logs + expose: + - "8080" + + caddy: + image: caddy:2-alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + + bugsink: + image: bugsink/bugsink + restart: unless-stopped + env_file: .env + environment: + SECRET_KEY: ${BUGSINK_SECRET_KEY} + CREATE_SUPERUSER: ${BUGSINK_SUPERUSER} + PORT: 8000 + BEHIND_HTTPS_PROXY: "True" + expose: + - "8000" + +volumes: + logs: + caddy_data: + caddy_config: diff --git a/server/go.mod b/server/go.mod index 561f8375..a2bc743b 100644 --- a/server/go.mod +++ b/server/go.mod @@ -2,12 +2,4 @@ module github.com/edde746/plezy-relay go 1.22 -require ( - github.com/gorilla/websocket v1.5.3 - golang.org/x/crypto v0.31.0 -) - -require ( - golang.org/x/net v0.33.0 // indirect - golang.org/x/text v0.21.0 // indirect -) +require github.com/gorilla/websocket v1.5.3 diff --git a/server/go.sum b/server/go.sum index 60532402..25a9fc4b 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,8 +1,2 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= diff --git a/server/main.go b/server/main.go index d2d90a81..dbca1c8d 100644 --- a/server/main.go +++ b/server/main.go @@ -9,12 +9,14 @@ import ( "math/big" "net" "net/http" + "os" + "path/filepath" + "strconv" "strings" "sync" "time" "github.com/gorilla/websocket" - "golang.org/x/crypto/acme/autocert" ) const ( @@ -29,13 +31,16 @@ const ( pingInterval = 30 * time.Second maxMessageSize = 64 * 1024 maxLogSize = 1 * 1024 * 1024 // 1MB - logMaxAge = 7 * 24 * time.Hour + logMaxAge = 3 * 24 * time.Hour logIDLength = 5 logRateInterval = 1 * time.Minute + maxLogEntries = 500 ) var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { return true }, } // --- Rate limiter (token bucket) --- @@ -150,21 +155,36 @@ func (r *Room) sendTo(targetID string, msg serverMsg) bool { // --- Log store --- type logEntry struct { - Text string + Size int ExpiresAt time.Time } type logStore struct { entries map[string]logEntry rateLimit map[string]time.Time // IP -> last upload time + dir string mu sync.RWMutex } -func newLogStore() *logStore { - return &logStore{ +func newLogStore(dir string) *logStore { + if err := os.MkdirAll(dir, 0755); err != nil { + log.Fatalf("failed to create log dir %s: %v", dir, err) + } + ls := &logStore{ entries: make(map[string]logEntry), rateLimit: make(map[string]time.Time), + dir: dir, } + // Clean orphaned files from prior runs + files, _ := os.ReadDir(dir) + for _, f := range files { + os.Remove(filepath.Join(dir, f.Name())) + } + return ls +} + +func (ls *logStore) filePath(id string) string { + return filepath.Join(ls.dir, id+".log") } const logIDChars = "abcdefghijklmnopqrstuvwxyz0123456789" @@ -184,6 +204,7 @@ func (ls *logStore) cleanup() { now := time.Now() for id, entry := range ls.entries { if now.After(entry.ExpiresAt) { + os.Remove(ls.filePath(id)) delete(ls.entries, id) } } @@ -202,8 +223,8 @@ type Server struct { mu sync.RWMutex } -func newServer() *Server { - s := &Server{rooms: make(map[string]*Room), logs: newLogStore()} +func newServer(logDir string) *Server { + s := &Server{rooms: make(map[string]*Room), logs: newLogStore(logDir)} go s.cleanupLoop() return s } @@ -271,10 +292,24 @@ func (s *Server) handlePostLogs(w http.ResponseWriter, r *http.Request) { return } + s.logs.mu.Lock() + if len(s.logs.entries) >= maxLogEntries { + s.logs.mu.Unlock() + http.Error(w, "Log store full", http.StatusServiceUnavailable) + return + } + s.logs.mu.Unlock() + id := generateLogID() + if err := os.WriteFile(s.logs.filePath(id), body, 0644); err != nil { + log.Printf("logs: failed to write %s: %v", id, err) + http.Error(w, "Failed to store log", http.StatusInternalServerError) + return + } + s.logs.mu.Lock() s.logs.entries[id] = logEntry{ - Text: string(body), + Size: len(body), ExpiresAt: time.Now().Add(logMaxAge), } s.logs.mu.Unlock() @@ -306,8 +341,15 @@ func (s *Server) handleGetLogs(w http.ResponseWriter, r *http.Request) { return } + data, err := os.ReadFile(s.logs.filePath(id)) + if err != nil { + http.Error(w, "Not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Write([]byte(entry.Text)) + w.Header().Set("Content-Length", strconv.Itoa(entry.Size)) + w.Write(data) } func (s *Server) sendError(conn *websocket.Conn, code, message string) { @@ -485,11 +527,11 @@ func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) { } func main() { - dev := flag.Bool("dev", false, "Run in development mode (plain HTTP on :8080)") - host := flag.String("host", "ice.plezy.app", "Hostname for TLS autocert") + addr := flag.String("addr", ":8080", "Listen address") + logDir := flag.String("log-dir", "/data/logs", "Directory for log file storage") flag.Parse() - srv := newServer() + srv := newServer(*logDir) mux := http.NewServeMux() mux.HandleFunc("/relay", srv.handleWS) @@ -500,29 +542,6 @@ func main() { mux.HandleFunc("/logs", srv.handlePostLogs) mux.HandleFunc("/logs/", srv.handleGetLogs) - if *dev { - log.Println("Starting dev server on :8080") - log.Fatal(http.ListenAndServe(":8080", mux)) - } else { - certManager := autocert.Manager{ - Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist(*host), - Cache: autocert.DirCache("/var/lib/plezy-relay/certs"), - } - - server := &http.Server{ - Addr: ":443", - Handler: mux, - TLSConfig: certManager.TLSConfig(), - } - - // HTTP challenge server for Let's Encrypt - go func() { - log.Println("Starting HTTP challenge server on :80") - log.Fatal(http.ListenAndServe(":80", certManager.HTTPHandler(nil))) - }() - - log.Printf("Starting relay server on :443 (host=%s)", *host) - log.Fatal(server.ListenAndServeTLS("", "")) - } + log.Printf("Starting relay server on %s", *addr) + log.Fatal(http.ListenAndServe(*addr, mux)) } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 6eeee254..da9735bf 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("OsMediaControlsPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + SentryFlutterPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SentryFlutterPlugin")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); GamepadPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 85ff0401..c80b059b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus os_media_controls screen_retriever_windows + sentry_flutter sqlite3_flutter_libs universal_gamepad url_launcher_windows