From 414e5804eb2d1dbca9351ae7ddf1feacbbb14807 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 26 Feb 2026 05:48:32 +0100 Subject: [PATCH] feat: add hide spoilers setting for unwatched episodes --- lib/i18n/de.i18n.json | 2 ++ lib/i18n/en.i18n.json | 2 ++ lib/i18n/es.i18n.json | 2 ++ lib/i18n/fr.i18n.json | 2 ++ lib/i18n/it.i18n.json | 2 ++ lib/i18n/ko.i18n.json | 2 ++ lib/i18n/nl.i18n.json | 2 ++ lib/i18n/strings_de.g.dart | 4 +++ lib/i18n/strings_en.g.dart | 8 +++++ lib/i18n/strings_es.g.dart | 4 +++ lib/i18n/strings_fr.g.dart | 4 +++ lib/i18n/strings_it.g.dart | 4 +++ lib/i18n/strings_ko.g.dart | 4 +++ lib/i18n/strings_nl.g.dart | 4 +++ lib/i18n/strings_sv.g.dart | 4 +++ lib/i18n/strings_zh.g.dart | 4 +++ lib/i18n/sv.i18n.json | 2 ++ lib/i18n/zh.i18n.json | 2 ++ lib/providers/settings_provider.dart | 10 ++++++ lib/screens/season_detail_screen.dart | 22 +++++++++++-- lib/screens/settings/settings_screen.dart | 15 +++++++++ lib/services/settings_service.dart | 11 +++++++ lib/utils/content_utils.dart | 9 +++++ lib/widgets/media_card.dart | 40 ++++++++++++++++------- 24 files changed, 150 insertions(+), 15 deletions(-) diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index b8a04f1f..f4fb1154 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "Seitenleiste bleibt erweitert und Inhaltsbereich passt sich an", "showUnwatchedCount": "Anzahl nicht gesehener Folgen anzeigen", "showUnwatchedCountDescription": "Zeigt die Anzahl nicht gesehener Episoden bei Serien und Staffeln an", + "hideSpoilers": "Spoiler für nicht gesehene Episoden verbergen", + "hideSpoilersDescription": "Vorschaubilder unscharf machen und Beschreibungen für noch nicht gesehene Episoden ausblenden", "playerBackend": "Player-Backend", "exoPlayer": "ExoPlayer (Empfohlen)", "exoPlayerDescription": "Android-nativer Player mit besserer Hardware-Unterstützung", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 646a4b08..dd069605 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "Sidebar stays expanded and content area adjusts to fit", "showUnwatchedCount": "Show Unwatched Count", "showUnwatchedCountDescription": "Display unwatched episode count on shows and seasons", + "hideSpoilers": "Hide Spoilers for Unwatched Episodes", + "hideSpoilersDescription": "Blur thumbnails and hide descriptions for episodes you haven't watched yet", "playerBackend": "Player Backend", "exoPlayer": "ExoPlayer (Recommended)", "exoPlayerDescription": "Android native player with better hardware support", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 80173972..3440384f 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "La barra lateral permanece expandida y el área de contenido se ajusta para adaptarse", "showUnwatchedCount": "Mostrar conteo de no vistos", "showUnwatchedCountDescription": "Mostrar el conteo de episodios no vistos en series y temporadas", + "hideSpoilers": "Ocultar spoilers de episodios no vistos", + "hideSpoilersDescription": "Difuminar miniaturas y ocultar descripciones de episodios que aún no has visto", "playerBackend": "Reproductor", "exoPlayer": "ExoPlayer (Recomendado)", "exoPlayerDescription": "Reproductor nativo de Android con mejor soporte de hardware", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 93e508af..585b23e0 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "La barre latérale reste étendue et la zone de contenu s'adapte", "showUnwatchedCount": "Afficher le nombre non visionné", "showUnwatchedCountDescription": "Afficher le nombre d'épisodes non visionnés pour les séries et saisons", + "hideSpoilers": "Masquer les spoilers des épisodes non vus", + "hideSpoilersDescription": "Flouter les miniatures et masquer les descriptions des épisodes que vous n'avez pas encore regardés", "playerBackend": "Moteur de lecture", "exoPlayer": "ExoPlayer (Recommandé)", "exoPlayerDescription": "Lecteur natif Android avec meilleur support matériel", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 971139c9..9c787968 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "La barra laterale rimane espansa e l'area del contenuto si adatta", "showUnwatchedCount": "Mostra conteggio non visti", "showUnwatchedCountDescription": "Mostra il numero di episodi non visti per serie e stagioni", + "hideSpoilers": "Nascondi spoiler per episodi non visti", + "hideSpoilersDescription": "Sfoca le miniature e nascondi le descrizioni degli episodi che non hai ancora guardato", "playerBackend": "Motore di riproduzione", "exoPlayer": "ExoPlayer (Consigliato)", "exoPlayerDescription": "Lettore nativo Android con migliore supporto hardware", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 989506d2..fa95896b 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "사이드바가 확장된 상태로 유지되고 콘텐츠 영역이 맞춰집니다", "showUnwatchedCount": "미시청 수 표시", "showUnwatchedCountDescription": "시리즈 및 시즌에 미시청 에피소드 수 표시", + "hideSpoilers": "미시청 에피소드 스포일러 숨기기", + "hideSpoilersDescription": "아직 시청하지 않은 에피소드의 썸네일을 흐리게 하고 설명을 숨깁니다", "playerBackend": "플레이어 백엔드", "exoPlayer": "ExoPlayer (권장)", "exoPlayerDescription": "더 나은 하드웨어 지원을 제공하는 Android 네이티브 플레이어", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 4024209a..6269ccd2 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "Zijbalk blijft uitgevouwen en inhoudsgebied past zich aan", "showUnwatchedCount": "Aantal ongekeken tonen", "showUnwatchedCountDescription": "Toon aantal ongekeken afleveringen bij series en seizoenen", + "hideSpoilers": "Spoilers voor ongekeken afleveringen verbergen", + "hideSpoilersDescription": "Miniaturen vervagen en beschrijvingen verbergen voor afleveringen die je nog niet hebt gezien", "playerBackend": "Speler backend", "exoPlayer": "ExoPlayer (Aanbevolen)", "exoPlayerDescription": "Android-native speler met betere hardware-ondersteuning", diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 0c096bfd..8a362f19 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -236,6 +236,8 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn { @override String get alwaysKeepSidebarOpenDescription => 'Seitenleiste bleibt erweitert und Inhaltsbereich passt sich an'; @override String get showUnwatchedCount => 'Anzahl nicht gesehener Folgen anzeigen'; @override String get showUnwatchedCountDescription => 'Zeigt die Anzahl nicht gesehener Episoden bei Serien und Staffeln an'; + @override String get hideSpoilers => 'Spoiler für nicht gesehene Episoden verbergen'; + @override String get hideSpoilersDescription => 'Vorschaubilder unscharf machen und Beschreibungen für noch nicht gesehene Episoden ausblenden'; @override String get playerBackend => 'Player-Backend'; @override String get exoPlayer => 'ExoPlayer (Empfohlen)'; @override String get exoPlayerDescription => 'Android-nativer Player mit besserer Hardware-Unterstützung'; @@ -1394,6 +1396,8 @@ extension on TranslationsDe { 'settings.alwaysKeepSidebarOpenDescription' => 'Seitenleiste bleibt erweitert und Inhaltsbereich passt sich an', 'settings.showUnwatchedCount' => 'Anzahl nicht gesehener Folgen anzeigen', 'settings.showUnwatchedCountDescription' => 'Zeigt die Anzahl nicht gesehener Episoden bei Serien und Staffeln an', + 'settings.hideSpoilers' => 'Spoiler für nicht gesehene Episoden verbergen', + 'settings.hideSpoilersDescription' => 'Vorschaubilder unscharf machen und Beschreibungen für noch nicht gesehene Episoden ausblenden', 'settings.playerBackend' => 'Player-Backend', 'settings.exoPlayer' => 'ExoPlayer (Empfohlen)', 'settings.exoPlayerDescription' => 'Android-nativer Player mit besserer Hardware-Unterstützung', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 061f0d0d..2da53df6 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -454,6 +454,12 @@ class TranslationsSettingsEn { /// en: 'Display unwatched episode count on shows and seasons' String get showUnwatchedCountDescription => 'Display unwatched episode count on shows and seasons'; + /// 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' + String get hideSpoilersDescription => 'Blur thumbnails and hide descriptions for episodes you haven\'t watched yet'; + /// en: 'Player Backend' String get playerBackend => 'Player Backend'; @@ -2982,6 +2988,8 @@ extension on Translations { 'settings.alwaysKeepSidebarOpenDescription' => 'Sidebar stays expanded and content area adjusts to fit', 'settings.showUnwatchedCount' => 'Show Unwatched Count', 'settings.showUnwatchedCountDescription' => 'Display unwatched episode count on shows and seasons', + 'settings.hideSpoilers' => 'Hide Spoilers for Unwatched Episodes', + 'settings.hideSpoilersDescription' => 'Blur thumbnails and hide descriptions for episodes you haven\'t watched yet', 'settings.playerBackend' => 'Player Backend', 'settings.exoPlayer' => 'ExoPlayer (Recommended)', 'settings.exoPlayerDescription' => 'Android native player with better hardware support', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 824569b0..6d5f756c 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -236,6 +236,8 @@ class _TranslationsSettingsEs implements TranslationsSettingsEn { @override String get alwaysKeepSidebarOpenDescription => 'La barra lateral permanece expandida y el área de contenido se ajusta para adaptarse'; @override String get showUnwatchedCount => 'Mostrar conteo de no vistos'; @override String get showUnwatchedCountDescription => 'Mostrar el conteo de episodios no vistos en series y temporadas'; + @override String get hideSpoilers => 'Ocultar spoilers de episodios no vistos'; + @override String get hideSpoilersDescription => 'Difuminar miniaturas y ocultar descripciones de episodios que aún no has visto'; @override String get playerBackend => 'Reproductor'; @override String get exoPlayer => 'ExoPlayer (Recomendado)'; @override String get exoPlayerDescription => 'Reproductor nativo de Android con mejor soporte de hardware'; @@ -1394,6 +1396,8 @@ extension on TranslationsEs { 'settings.alwaysKeepSidebarOpenDescription' => 'La barra lateral permanece expandida y el área de contenido se ajusta para adaptarse', 'settings.showUnwatchedCount' => 'Mostrar conteo de no vistos', 'settings.showUnwatchedCountDescription' => 'Mostrar el conteo de episodios no vistos en series y temporadas', + 'settings.hideSpoilers' => 'Ocultar spoilers de episodios no vistos', + 'settings.hideSpoilersDescription' => 'Difuminar miniaturas y ocultar descripciones de episodios que aún no has visto', 'settings.playerBackend' => 'Reproductor', 'settings.exoPlayer' => 'ExoPlayer (Recomendado)', 'settings.exoPlayerDescription' => 'Reproductor nativo de Android con mejor soporte de hardware', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index c9416902..53dab1ac 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -236,6 +236,8 @@ class _TranslationsSettingsFr implements TranslationsSettingsEn { @override String get alwaysKeepSidebarOpenDescription => 'La barre latérale reste étendue et la zone de contenu s\'adapte'; @override String get showUnwatchedCount => 'Afficher le nombre non visionné'; @override String get showUnwatchedCountDescription => 'Afficher le nombre d\'épisodes non visionnés pour les séries et saisons'; + @override String get hideSpoilers => 'Masquer les spoilers des épisodes non vus'; + @override String get hideSpoilersDescription => 'Flouter les miniatures et masquer les descriptions des épisodes que vous n\'avez pas encore regardés'; @override String get playerBackend => 'Moteur de lecture'; @override String get exoPlayer => 'ExoPlayer (Recommandé)'; @override String get exoPlayerDescription => 'Lecteur natif Android avec meilleur support matériel'; @@ -1394,6 +1396,8 @@ extension on TranslationsFr { 'settings.alwaysKeepSidebarOpenDescription' => 'La barre latérale reste étendue et la zone de contenu s\'adapte', 'settings.showUnwatchedCount' => 'Afficher le nombre non visionné', 'settings.showUnwatchedCountDescription' => 'Afficher le nombre d\'épisodes non visionnés pour les séries et saisons', + 'settings.hideSpoilers' => 'Masquer les spoilers des épisodes non vus', + 'settings.hideSpoilersDescription' => 'Flouter les miniatures et masquer les descriptions des épisodes que vous n\'avez pas encore regardés', 'settings.playerBackend' => 'Moteur de lecture', 'settings.exoPlayer' => 'ExoPlayer (Recommandé)', 'settings.exoPlayerDescription' => 'Lecteur natif Android avec meilleur support matériel', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index ee2377a2..20ea62c6 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -236,6 +236,8 @@ class _TranslationsSettingsIt implements TranslationsSettingsEn { @override String get alwaysKeepSidebarOpenDescription => 'La barra laterale rimane espansa e l\'area del contenuto si adatta'; @override String get showUnwatchedCount => 'Mostra conteggio non visti'; @override String get showUnwatchedCountDescription => 'Mostra il numero di episodi non visti per serie e stagioni'; + @override String get hideSpoilers => 'Nascondi spoiler per episodi non visti'; + @override String get hideSpoilersDescription => 'Sfoca le miniature e nascondi le descrizioni degli episodi che non hai ancora guardato'; @override String get playerBackend => 'Motore di riproduzione'; @override String get exoPlayer => 'ExoPlayer (Consigliato)'; @override String get exoPlayerDescription => 'Lettore nativo Android con migliore supporto hardware'; @@ -1394,6 +1396,8 @@ extension on TranslationsIt { 'settings.alwaysKeepSidebarOpenDescription' => 'La barra laterale rimane espansa e l\'area del contenuto si adatta', 'settings.showUnwatchedCount' => 'Mostra conteggio non visti', 'settings.showUnwatchedCountDescription' => 'Mostra il numero di episodi non visti per serie e stagioni', + 'settings.hideSpoilers' => 'Nascondi spoiler per episodi non visti', + 'settings.hideSpoilersDescription' => 'Sfoca le miniature e nascondi le descrizioni degli episodi che non hai ancora guardato', 'settings.playerBackend' => 'Motore di riproduzione', 'settings.exoPlayer' => 'ExoPlayer (Consigliato)', 'settings.exoPlayerDescription' => 'Lettore nativo Android con migliore supporto hardware', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index bbf1c283..c34fb3f1 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -236,6 +236,8 @@ class _TranslationsSettingsKo implements TranslationsSettingsEn { @override String get alwaysKeepSidebarOpenDescription => '사이드바가 확장된 상태로 유지되고 콘텐츠 영역이 맞춰집니다'; @override String get showUnwatchedCount => '미시청 수 표시'; @override String get showUnwatchedCountDescription => '시리즈 및 시즌에 미시청 에피소드 수 표시'; + @override String get hideSpoilers => '미시청 에피소드 스포일러 숨기기'; + @override String get hideSpoilersDescription => '아직 시청하지 않은 에피소드의 썸네일을 흐리게 하고 설명을 숨깁니다'; @override String get playerBackend => '플레이어 백엔드'; @override String get exoPlayer => 'ExoPlayer (권장)'; @override String get exoPlayerDescription => '더 나은 하드웨어 지원을 제공하는 Android 네이티브 플레이어'; @@ -1394,6 +1396,8 @@ extension on TranslationsKo { 'settings.alwaysKeepSidebarOpenDescription' => '사이드바가 확장된 상태로 유지되고 콘텐츠 영역이 맞춰집니다', 'settings.showUnwatchedCount' => '미시청 수 표시', 'settings.showUnwatchedCountDescription' => '시리즈 및 시즌에 미시청 에피소드 수 표시', + 'settings.hideSpoilers' => '미시청 에피소드 스포일러 숨기기', + 'settings.hideSpoilersDescription' => '아직 시청하지 않은 에피소드의 썸네일을 흐리게 하고 설명을 숨깁니다', 'settings.playerBackend' => '플레이어 백엔드', 'settings.exoPlayer' => 'ExoPlayer (권장)', 'settings.exoPlayerDescription' => '더 나은 하드웨어 지원을 제공하는 Android 네이티브 플레이어', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 281473fe..da12c779 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -236,6 +236,8 @@ class _TranslationsSettingsNl implements TranslationsSettingsEn { @override String get alwaysKeepSidebarOpenDescription => 'Zijbalk blijft uitgevouwen en inhoudsgebied past zich aan'; @override String get showUnwatchedCount => 'Aantal ongekeken tonen'; @override String get showUnwatchedCountDescription => 'Toon aantal ongekeken afleveringen bij series en seizoenen'; + @override String get hideSpoilers => 'Spoilers voor ongekeken afleveringen verbergen'; + @override String get hideSpoilersDescription => 'Miniaturen vervagen en beschrijvingen verbergen voor afleveringen die je nog niet hebt gezien'; @override String get playerBackend => 'Speler backend'; @override String get exoPlayer => 'ExoPlayer (Aanbevolen)'; @override String get exoPlayerDescription => 'Android-native speler met betere hardware-ondersteuning'; @@ -1394,6 +1396,8 @@ extension on TranslationsNl { 'settings.alwaysKeepSidebarOpenDescription' => 'Zijbalk blijft uitgevouwen en inhoudsgebied past zich aan', 'settings.showUnwatchedCount' => 'Aantal ongekeken tonen', 'settings.showUnwatchedCountDescription' => 'Toon aantal ongekeken afleveringen bij series en seizoenen', + 'settings.hideSpoilers' => 'Spoilers voor ongekeken afleveringen verbergen', + 'settings.hideSpoilersDescription' => 'Miniaturen vervagen en beschrijvingen verbergen voor afleveringen die je nog niet hebt gezien', 'settings.playerBackend' => 'Speler backend', 'settings.exoPlayer' => 'ExoPlayer (Aanbevolen)', 'settings.exoPlayerDescription' => 'Android-native speler met betere hardware-ondersteuning', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 8a7b1380..b7d2868b 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -236,6 +236,8 @@ class _TranslationsSettingsSv implements TranslationsSettingsEn { @override String get alwaysKeepSidebarOpenDescription => 'Sidofältet förblir expanderat och innehållsytan anpassas'; @override String get showUnwatchedCount => 'Visa antal osedda'; @override String get showUnwatchedCountDescription => 'Visa antal osedda avsnitt för serier och säsonger'; + @override String get hideSpoilers => 'Dölj spoilers för osedda avsnitt'; + @override String get hideSpoilersDescription => 'Gör miniatyrer suddiga och dölj beskrivningar för avsnitt du inte har sett ännu'; @override String get playerBackend => 'Spelarmotor'; @override String get exoPlayer => 'ExoPlayer (Rekommenderad)'; @override String get exoPlayerDescription => 'Android-nativ spelare med bättre hårdvarustöd'; @@ -1394,6 +1396,8 @@ extension on TranslationsSv { 'settings.alwaysKeepSidebarOpenDescription' => 'Sidofältet förblir expanderat och innehållsytan anpassas', 'settings.showUnwatchedCount' => 'Visa antal osedda', 'settings.showUnwatchedCountDescription' => 'Visa antal osedda avsnitt för serier och säsonger', + 'settings.hideSpoilers' => 'Dölj spoilers för osedda avsnitt', + 'settings.hideSpoilersDescription' => 'Gör miniatyrer suddiga och dölj beskrivningar för avsnitt du inte har sett ännu', 'settings.playerBackend' => 'Spelarmotor', 'settings.exoPlayer' => 'ExoPlayer (Rekommenderad)', 'settings.exoPlayerDescription' => 'Android-nativ spelare med bättre hårdvarustöd', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index a79bc4ce..37a6fff0 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -236,6 +236,8 @@ class _TranslationsSettingsZh implements TranslationsSettingsEn { @override String get alwaysKeepSidebarOpenDescription => '侧边栏保持展开状态,内容区域自动调整'; @override String get showUnwatchedCount => '显示未观看数量'; @override String get showUnwatchedCountDescription => '在剧集和季上显示未观看的集数'; + @override String get hideSpoilers => '隐藏未看剧集的剧透内容'; + @override String get hideSpoilersDescription => '模糊未观看剧集的缩略图并隐藏其描述'; @override String get playerBackend => '播放器引擎'; @override String get exoPlayer => 'ExoPlayer(推荐)'; @override String get exoPlayerDescription => 'Android 原生播放器,硬件支持更好'; @@ -1394,6 +1396,8 @@ extension on TranslationsZh { 'settings.alwaysKeepSidebarOpenDescription' => '侧边栏保持展开状态,内容区域自动调整', 'settings.showUnwatchedCount' => '显示未观看数量', 'settings.showUnwatchedCountDescription' => '在剧集和季上显示未观看的集数', + 'settings.hideSpoilers' => '隐藏未看剧集的剧透内容', + 'settings.hideSpoilersDescription' => '模糊未观看剧集的缩略图并隐藏其描述', 'settings.playerBackend' => '播放器引擎', 'settings.exoPlayer' => 'ExoPlayer(推荐)', 'settings.exoPlayerDescription' => 'Android 原生播放器,硬件支持更好', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 02deee9d..2f52a17e 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "Sidofältet förblir expanderat och innehållsytan anpassas", "showUnwatchedCount": "Visa antal osedda", "showUnwatchedCountDescription": "Visa antal osedda avsnitt för serier och säsonger", + "hideSpoilers": "Dölj spoilers för osedda avsnitt", + "hideSpoilersDescription": "Gör miniatyrer suddiga och dölj beskrivningar för avsnitt du inte har sett ännu", "playerBackend": "Spelarmotor", "exoPlayer": "ExoPlayer (Rekommenderad)", "exoPlayerDescription": "Android-nativ spelare med bättre hårdvarustöd", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index b8243920..3453e72a 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -117,6 +117,8 @@ "alwaysKeepSidebarOpenDescription": "侧边栏保持展开状态,内容区域自动调整", "showUnwatchedCount": "显示未观看数量", "showUnwatchedCountDescription": "在剧集和季上显示未观看的集数", + "hideSpoilers": "隐藏未看剧集的剧透内容", + "hideSpoilersDescription": "模糊未观看剧集的缩略图并隐藏其描述", "playerBackend": "播放器引擎", "exoPlayer": "ExoPlayer(推荐)", "exoPlayerDescription": "Android 原生播放器,硬件支持更好", diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index bf3cadde..a0694355 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -12,6 +12,7 @@ class SettingsProvider extends ChangeNotifier { bool _showServerNameOnHubs = false; bool _alwaysKeepSidebarOpen = false; bool _showUnwatchedCount = true; + bool _hideSpoilers = false; bool _isInitialized = false; Future? _initFuture; @@ -36,6 +37,7 @@ class SettingsProvider extends ChangeNotifier { _showServerNameOnHubs = _settingsService!.getShowServerNameOnHubs(); _alwaysKeepSidebarOpen = _settingsService!.getAlwaysKeepSidebarOpen(); _showUnwatchedCount = _settingsService!.getShowUnwatchedCount(); + _hideSpoilers = _settingsService!.getHideSpoilers(); _isInitialized = true; notifyListeners(); } @@ -59,6 +61,8 @@ class SettingsProvider extends ChangeNotifier { bool get showUnwatchedCount => _showUnwatchedCount; + bool get hideSpoilers => _hideSpoilers; + /// Helper to update a setting: ensures init, deduplicates, persists, notifies. Future _updateSetting({ required T current, @@ -122,6 +126,12 @@ class SettingsProvider extends ChangeNotifier { persist: _settingsService!.setShowUnwatchedCount, ); + Future setHideSpoilers(bool value) => _updateSetting( + current: _hideSpoilers, value: value, + setLocal: (v) => _hideSpoilers = v, + persist: _settingsService!.setHideSpoilers, + ); + String get libraryDensityDisplayName { switch (_libraryDensity) { case LibraryDensity.compact: diff --git a/lib/screens/season_detail_screen.dart b/lib/screens/season_detail_screen.dart index 9336b884..13ad8091 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/screens/season_detail_screen.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,6 +15,8 @@ import '../focus/dpad_navigator.dart'; import '../focus/input_mode_tracker.dart'; import '../models/download_models.dart'; import '../providers/download_provider.dart'; +import '../providers/settings_provider.dart'; +import '../utils/content_utils.dart'; import '../services/download_storage_service.dart'; import '../widgets/plex_optimized_image.dart'; import '../models/plex_metadata.dart'; @@ -383,6 +386,9 @@ class _EpisodeCardState extends State<_EpisodeCard> { @override Widget build(BuildContext context) { + final hideSpoilers = context.watch().hideSpoilers; + final shouldBlur = hideSpoilers && widget.episode.shouldHideSpoiler; + // Hide progress when offline (not tracked) final hasProgress = !widget.isOffline && @@ -430,7 +436,17 @@ class _EpisodeCardState extends State<_EpisodeCard> { children: [ ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(6)), - child: AspectRatio(aspectRatio: 16 / 9, child: _buildEpisodeThumbnail()), + child: AspectRatio( + aspectRatio: 16 / 9, + child: shouldBlur + ? ClipRect( + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: _buildEpisodeThumbnail(), + ), + ) + : _buildEpisodeThumbnail(), + ), ), // Play overlay @@ -634,8 +650,8 @@ class _EpisodeCardState extends State<_EpisodeCard> { }, ), - // Summary - if (widget.episode.summary != null && widget.episode.summary!.isNotEmpty) ...[ + // Summary (hidden when spoiler protection is active) + if (!shouldBlur && widget.episode.summary != null && widget.episode.summary!.isNotEmpty) ...[ const SizedBox(height: 6), Text( widget.episode.summary!, diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index a7dc66a0..b2629ff8 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -71,6 +71,7 @@ class _SettingsScreenState extends State with FocusableTab { static const _kShowServerNameOnHubs = 'show_server_name_on_hubs'; static const _kAlwaysKeepSidebarOpen = 'always_keep_sidebar_open'; static const _kShowUnwatchedCount = 'show_unwatched_count'; + static const _kHideSpoilers = 'hide_spoilers'; static const _kRequireProfileSelectionOnOpen = 'require_profile_selection_on_open'; static const _kConfirmExitOnBack = 'confirm_exit_on_back'; static const _kPlayerBackend = 'player_backend'; @@ -383,6 +384,20 @@ class _SettingsScreenState extends State with FocusableTab { ); }, ), + Consumer( + builder: (context, settingsProvider, child) { + return SwitchListTile( + focusNode: _focusTracker.get(_kHideSpoilers), + secondary: const AppIcon(Symbols.visibility_off_rounded, fill: 1), + title: Text(t.settings.hideSpoilers), + subtitle: Text(t.settings.hideSpoilersDescription), + value: settingsProvider.hideSpoilers, + onChanged: (value) async { + await settingsProvider.setHideSpoilers(value); + }, + ); + }, + ), Consumer( builder: (context, userProfileProvider, child) { if (!userProfileProvider.hasMultipleUsers) return const SizedBox.shrink(); diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 518a9c67..4b830db9 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -70,6 +70,7 @@ class SettingsService extends BaseSharedPreferencesService { static const String _keyUseExoPlayer = 'use_exoplayer'; static const String _keyAlwaysKeepSidebarOpen = 'always_keep_sidebar_open'; static const String _keyShowUnwatchedCount = 'show_unwatched_count'; + static const String _keyHideSpoilers = 'hide_spoilers'; static const String _keyGlobalShaderPreset = 'global_shader_preset'; static const String _keyRequireProfileSelectionOnOpen = 'require_profile_selection_on_open'; static const String _keyUseExternalPlayer = 'use_external_player'; @@ -1037,6 +1038,15 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getBool(_keyShowUnwatchedCount) ?? true; // Default: enabled (show counts) } + // Hide Spoilers (blur thumbnails and hide descriptions for unwatched episodes) + Future setHideSpoilers(bool enabled) async { + await prefs.setBool(_keyHideSpoilers, enabled); + } + + bool getHideSpoilers() { + return prefs.getBool(_keyHideSpoilers) ?? false; // Default: disabled + } + // Global Shader Preset (for MPV video enhancement) Future setGlobalShaderPreset(String presetId) async { await prefs.setString(_keyGlobalShaderPreset, presetId); @@ -1187,6 +1197,7 @@ class SettingsService extends BaseSharedPreferencesService { prefs.remove(_keyUseExoPlayer), prefs.remove(_keyAlwaysKeepSidebarOpen), prefs.remove(_keyShowUnwatchedCount), + prefs.remove(_keyHideSpoilers), prefs.remove(_keyGlobalShaderPreset), prefs.remove(_keyRequireProfileSelectionOnOpen), prefs.remove(_keyUseExternalPlayer), diff --git a/lib/utils/content_utils.dart b/lib/utils/content_utils.dart index 27618325..a17b5481 100644 --- a/lib/utils/content_utils.dart +++ b/lib/utils/content_utils.dart @@ -100,4 +100,13 @@ extension PlexMetadataType on PlexMetadata { bool get isClip => _lowerType == ContentTypes.clip; bool get isMusicContent => ContentTypes.musicTypes.contains(_lowerType); bool get isVideoContent => ContentTypes.videoTypes.contains(_lowerType); + + /// Whether this episode should have spoiler protection applied. + /// True when the item is an unwatched episode with no active progress. + bool get shouldHideSpoiler { + if (!isEpisode) return false; + if (isWatched) return false; + if (viewOffset != null && viewOffset! > 0) return false; + return true; + } } diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 2b9eb2fb..11c3a54b 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:plezy/utils/content_utils.dart'; import 'package:plezy/widgets/app_icon.dart'; @@ -557,8 +559,8 @@ class _MediaCardList extends StatelessWidget { ), const SizedBox(height: 4), ], - // Summary - if (item.summary != null) ...[ + // Summary (hidden when spoiler protection is active) + if (!(item is PlexMetadata && context.watch().hideSpoilers && (item as PlexMetadata).shouldHideSpoiler) && item.summary != null) ...[ Text( item.summary!, maxLines: _summaryMaxLines, @@ -605,12 +607,25 @@ Widget _buildPosterImage( localFilePath: localPosterPath, ); } else if (item is PlexMetadata) { - final episodePosterMode = context.watch().episodePosterMode; + final settingsProvider = context.watch(); + final episodePosterMode = settingsProvider.episodePosterMode; + final shouldBlur = settingsProvider.hideSpoilers && item.shouldHideSpoiler; posterUrl = item.posterThumb(mode: episodePosterMode, mixedHubContext: mixedHubContext); + Widget image; + // Use thumb image type for 16:9 content (episodes, or movies in mixed hubs) if (item.usesWideAspectRatio(episodePosterMode, mixedHubContext: mixedHubContext)) { - return PlexOptimizedImage.thumb( + image = PlexOptimizedImage.thumb( + client: isOffline ? null : context.getClientWithFallback(item.serverId), + imagePath: posterUrl, + width: knownWidth ?? double.infinity, + height: knownHeight ?? double.infinity, + fit: BoxFit.cover, + localFilePath: localPosterPath, + ); + } else { + image = PlexOptimizedImage.poster( client: isOffline ? null : context.getClientWithFallback(item.serverId), imagePath: posterUrl, width: knownWidth ?? double.infinity, @@ -620,14 +635,15 @@ Widget _buildPosterImage( ); } - return PlexOptimizedImage.poster( - client: isOffline ? null : context.getClientWithFallback(item.serverId), - imagePath: posterUrl, - width: knownWidth ?? double.infinity, - height: knownHeight ?? double.infinity, - fit: BoxFit.cover, - localFilePath: localPosterPath, - ); + if (shouldBlur) { + return ClipRect( + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: image, + ), + ); + } + return image; } return SkeletonLoader(