feat: add hide spoilers setting for unwatched episodes

This commit is contained in:
edde746
2026-02-26 11:55:45 +01:00
parent cbf9913e8a
commit 414e5804eb
24 changed files with 150 additions and 15 deletions
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -117,6 +117,8 @@
"alwaysKeepSidebarOpenDescription": "사이드바가 확장된 상태로 유지되고 콘텐츠 영역이 맞춰집니다",
"showUnwatchedCount": "미시청 수 표시",
"showUnwatchedCountDescription": "시리즈 및 시즌에 미시청 에피소드 수 표시",
"hideSpoilers": "미시청 에피소드 스포일러 숨기기",
"hideSpoilersDescription": "아직 시청하지 않은 에피소드의 썸네일을 흐리게 하고 설명을 숨깁니다",
"playerBackend": "플레이어 백엔드",
"exoPlayer": "ExoPlayer (권장)",
"exoPlayerDescription": "더 나은 하드웨어 지원을 제공하는 Android 네이티브 플레이어",
+2
View File
@@ -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",
+4
View File
@@ -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',
+8
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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 네이티브 플레이어',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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 原生播放器,硬件支持更好',
+2
View File
@@ -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",
+2
View File
@@ -117,6 +117,8 @@
"alwaysKeepSidebarOpenDescription": "侧边栏保持展开状态,内容区域自动调整",
"showUnwatchedCount": "显示未观看数量",
"showUnwatchedCountDescription": "在剧集和季上显示未观看的集数",
"hideSpoilers": "隐藏未看剧集的剧透内容",
"hideSpoilersDescription": "模糊未观看剧集的缩略图并隐藏其描述",
"playerBackend": "播放器引擎",
"exoPlayer": "ExoPlayer(推荐)",
"exoPlayerDescription": "Android 原生播放器,硬件支持更好",
+10
View File
@@ -12,6 +12,7 @@ class SettingsProvider extends ChangeNotifier {
bool _showServerNameOnHubs = false;
bool _alwaysKeepSidebarOpen = false;
bool _showUnwatchedCount = true;
bool _hideSpoilers = false;
bool _isInitialized = false;
Future<void>? _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<void> _updateSetting<T>({
required T current,
@@ -122,6 +126,12 @@ class SettingsProvider extends ChangeNotifier {
persist: _settingsService!.setShowUnwatchedCount,
);
Future<void> setHideSpoilers(bool value) => _updateSetting(
current: _hideSpoilers, value: value,
setLocal: (v) => _hideSpoilers = v,
persist: _settingsService!.setHideSpoilers,
);
String get libraryDensityDisplayName {
switch (_libraryDensity) {
case LibraryDensity.compact:
+19 -3
View File
@@ -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<SettingsProvider>().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!,
+15
View File
@@ -71,6 +71,7 @@ class _SettingsScreenState extends State<SettingsScreen> 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<SettingsScreen> with FocusableTab {
);
},
),
Consumer<SettingsProvider>(
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<UserProfileProvider>(
builder: (context, userProfileProvider, child) {
if (!userProfileProvider.hasMultipleUsers) return const SizedBox.shrink();
+11
View File
@@ -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<void> 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<void> 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),
+9
View File
@@ -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;
}
}
+28 -12
View File
@@ -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<SettingsProvider>().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<SettingsProvider>().episodePosterMode;
final settingsProvider = context.watch<SettingsProvider>();
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(