From 9d13584c00226537f1157bde6b45705ac2e531e4 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:57:49 +0200 Subject: [PATCH] feat(settings): add appearance toggle to hide the Explore tab The Explore tab appears for every Plex-backed profile because the Plex Discover catalog source connects implicitly, with no way to opt out short of a Jellyfin-only profile. Add a Show Explore Tab switch to Appearance > Navigation that hides the tab in the bottom navigation and side rail. UI-only: catalog sources stay connected so watchlist surfaces keep working while the tab is hidden. close #1844 --- lib/i18n/az.i18n.json | 2 ++ lib/i18n/bg.i18n.json | 2 ++ lib/i18n/da.i18n.json | 2 ++ 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/hu.i18n.json | 2 ++ lib/i18n/it.i18n.json | 2 ++ lib/i18n/ja.i18n.json | 2 ++ lib/i18n/kk.i18n.json | 2 ++ lib/i18n/ko.i18n.json | 2 ++ lib/i18n/nb.i18n.json | 2 ++ lib/i18n/nl.i18n.json | 2 ++ lib/i18n/pl.i18n.json | 2 ++ lib/i18n/pt.i18n.json | 2 ++ lib/i18n/ru.i18n.json | 2 ++ lib/i18n/strings.g.dart | 2 +- lib/i18n/strings_en.g.dart | 20 ++++++++++----- lib/i18n/sv.i18n.json | 2 ++ lib/i18n/tr.i18n.json | 2 ++ lib/i18n/uz.i18n.json | 2 ++ lib/i18n/zh-Hant.i18n.json | 2 ++ lib/i18n/zh.i18n.json | 2 ++ lib/screens/main_screen.dart | 13 ++++++++-- .../settings/appearance_settings_screen.dart | 13 +++++++++- lib/services/settings_service.dart | 6 +++++ lib/widgets/side_navigation_rail.dart | 8 +++--- test/widgets/side_navigation_rail_test.dart | 25 +++++++++++++++++++ 29 files changed, 118 insertions(+), 13 deletions(-) diff --git a/lib/i18n/az.i18n.json b/lib/i18n/az.i18n.json index 541686b1..2a876a96 100644 --- a/lib/i18n/az.i18n.json +++ b/lib/i18n/az.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Naviqasiya paneli yazılarını göstər", "showNavBarLabelsDescription": "Naviqasiya paneli ikonlarının altında mətni göstər", "startupSection": "Başlanğıc bölməsi", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Canlı TV-də sevimli kanalları defolt et", "liveTvDefaultFavoritesDescription": "Canlı TV açıldıqda yalnız sevimli kanalları göstər", "display": "Ekran", diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 83e693f2..2a447bcd 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Показвай етикети в навигационната лента", "showNavBarLabelsDescription": "Показвай текстови етикети под иконите в навигационната лента", "startupSection": "Начален раздел", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "По подразбиране към любими канали", "liveTvDefaultFavoritesDescription": "Показвай само любими канали при отваряне на телевизия на живо", "display": "Дисплей", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 478827e9..5f729374 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Vis tekst på navigationslinjen", "showNavBarLabelsDescription": "Vis tekst under ikonerne på navigationslinjen", "startupSection": "Startsektion", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Vis favoritkanaler som standard", "liveTvDefaultFavoritesDescription": "Vis kun favoritkanaler ved åbning af Live TV", "display": "Skærm", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 467423bd..cd2cee48 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Navigationsleisten-Beschriftungen anzeigen", "showNavBarLabelsDescription": "Textbeschriftungen unter den Symbolen der Navigationsleiste anzeigen", "startupSection": "Startbereich", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Standardmäßig Favoritenkanäle", "liveTvDefaultFavoritesDescription": "Beim Öffnen von Live TV nur Favoritenkanäle anzeigen", "display": "Anzeige", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index eacde9f7..12cbaf34 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Show Navigation Bar Labels", "showNavBarLabelsDescription": "Display text labels under navigation bar icons", "startupSection": "Startup Section", + "showExploreTab": "Show Explore Tab", + "showExploreTabDescription": "Display the Explore tab with content from Plex Discover and connected trackers", "liveTvDefaultFavorites": "Default to Favorite Channels", "liveTvDefaultFavoritesDescription": "Show only favorite channels when opening Live TV", "display": "Display", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 2d044bcb..6a36e10a 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Mostrar etiquetas de la barra de navegación", "showNavBarLabelsDescription": "Mostrar etiquetas de texto bajo los iconos de la barra de navegación", "startupSection": "Sección de inicio", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Canales favoritos por defecto", "liveTvDefaultFavoritesDescription": "Mostrar solo canales favoritos al abrir TV en vivo", "display": "Pantalla", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 8c9c3649..6ab765db 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Afficher les libellés de la barre de navigation", "showNavBarLabelsDescription": "Afficher les libellés sous les icônes de la barre de navigation", "startupSection": "Section de démarrage", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Chaînes favorites par défaut", "liveTvDefaultFavoritesDescription": "Afficher uniquement les chaînes favorites à l'ouverture de la TV en direct", "display": "Affichage", diff --git a/lib/i18n/hu.i18n.json b/lib/i18n/hu.i18n.json index 7c894239..0fe514c7 100644 --- a/lib/i18n/hu.i18n.json +++ b/lib/i18n/hu.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Navigációs sáv címkéinek megjelenítése", "showNavBarLabelsDescription": "Szöveges címkék megjelenítése a navigációs sáv ikonjai alatt", "startupSection": "Indítási oldal", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Kedvenc csatornák alapértelmezés szerint", "liveTvDefaultFavoritesDescription": "Csak a kedvenc csatornák megjelenítése az Élő TV megnyitásakor", "display": "Kijelző", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 408bb34d..09c471a5 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Mostra le etichette della barra di navigazione", "showNavBarLabelsDescription": "Mostra le etichette di testo sotto le icone della barra di navigazione", "startupSection": "Sezione di avvio", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Apri sui canali preferiti", "liveTvDefaultFavoritesDescription": "Mostra solo i canali preferiti quando apri la TV in diretta", "display": "Schermo", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index e9b90705..e3e191f4 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "ナビゲーションバーのラベルを表示", "showNavBarLabelsDescription": "ナビゲーションバーのアイコンの下にテキストラベルを表示", "startupSection": "起動時のセクション", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "お気に入りチャンネルをデフォルトに", "liveTvDefaultFavoritesDescription": "ライブTVを開いたときにお気に入りチャンネルのみ表示", "display": "ディスプレイ", diff --git a/lib/i18n/kk.i18n.json b/lib/i18n/kk.i18n.json index 93dc2225..8bdb7429 100644 --- a/lib/i18n/kk.i18n.json +++ b/lib/i18n/kk.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Навигация панелінің мәтіндерін көрсету", "showNavBarLabelsDescription": "Навигация белгішелерінің астында мәтінді көрсету", "startupSection": "Бастапқы бөлім", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Тікелей TV-де таңдаулы арналарды әдепкі ету", "liveTvDefaultFavoritesDescription": "Тікелей TV ашылғанда тек таңдаулы арналарды көрсету", "display": "Дисплей", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index ebec7516..e1b11cda 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "내비게이션 바 라벨 표시", "showNavBarLabelsDescription": "내비게이션 바 아이콘 아래에 텍스트 라벨을 표시합니다", "startupSection": "시작 섹션", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "즐겨찾기 채널 기본 설정", "liveTvDefaultFavoritesDescription": "라이브 TV를 열 때 즐겨찾기 채널만 표시", "display": "디스플레이", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index f7f364c4..f83c5840 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Vis etiketter i navigasjonsfeltet", "showNavBarLabelsDescription": "Vis tekstetiketter under ikonene i navigasjonsfeltet", "startupSection": "Startseksjon", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Vis favorittkanaler som standard", "liveTvDefaultFavoritesDescription": "Vis bare favorittkanaler når du åpner Direkte-TV", "display": "Skjerm", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index d9a23895..6bcac7c8 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Labels op navigatiebalk tonen", "showNavBarLabelsDescription": "Tekstlabels onder de pictogrammen op de navigatiebalk weergeven", "startupSection": "Opstartsectie", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Standaard favoriete zenders", "liveTvDefaultFavoritesDescription": "Toon alleen favoriete zenders bij het openen van Live TV", "display": "Weergave", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index fc65a781..a19ac353 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Pokaż etykiety paska nawigacji", "showNavBarLabelsDescription": "Wyświetl tekstowe etykiety pod ikonami paska nawigacji", "startupSection": "Sekcja startowa", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Domyślnie ulubione kanały", "liveTvDefaultFavoritesDescription": "Pokaż tylko ulubione kanały po otwarciu telewizji na żywo", "display": "Ekran", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 372648a9..ab5af90a 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Mostrar Rótulos da Barra de Navegação", "showNavBarLabelsDescription": "Exibir rótulos de texto sob os ícones da barra de navegação", "startupSection": "Seção inicial", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Canais favoritos por padrão", "liveTvDefaultFavoritesDescription": "Mostrar apenas os canais favoritos ao abrir a TV ao vivo", "display": "Tela", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index 8b375099..d4f67089 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Показывать подписи панели навигации", "showNavBarLabelsDescription": "Отображать текстовые подписи под иконками панели навигации", "startupSection": "Начальный раздел", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Избранные каналы по умолчанию", "liveTvDefaultFavoritesDescription": "Показывать только избранные каналы при открытии ТВ", "display": "Экран", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index c4ebdd6f..69f325a4 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 22 -/// Strings: 38354 (1743 per locale) +/// Strings: 38356 (1743 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index b9b07be5..1032acf9 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -1047,6 +1047,12 @@ class Translations$settings$en { /// en: 'Startup Section' String get startupSection => 'Startup Section'; + /// en: 'Show Explore Tab' + String get showExploreTab => 'Show Explore Tab'; + + /// en: 'Display the Explore tab with content from Plex Discover and connected trackers' + String get showExploreTabDescription => 'Display the Explore tab with content from Plex Discover and connected trackers'; + /// en: 'Default to Favorite Channels' String get liveTvDefaultFavorites => 'Default to Favorite Channels'; @@ -6384,6 +6390,8 @@ extension on Translations { 'settings.showNavBarLabels' => 'Show Navigation Bar Labels', 'settings.showNavBarLabelsDescription' => 'Display text labels under navigation bar icons', 'settings.startupSection' => 'Startup Section', + 'settings.showExploreTab' => 'Show Explore Tab', + 'settings.showExploreTabDescription' => 'Display the Explore tab with content from Plex Discover and connected trackers', 'settings.liveTvDefaultFavorites' => 'Default to Favorite Channels', 'settings.liveTvDefaultFavoritesDescription' => 'Show only favorite channels when opening Live TV', 'settings.display' => 'Display', @@ -6583,10 +6591,10 @@ extension on Translations { 'rateSheet.saved' => 'Saved', 'rateSheet.notAvailable' => 'No match found', 'rateSheet.noConnectedServices' => 'Connect a service in Settings to rate there.', - 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, movie', - 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV show', _ => null, } ?? switch (path) { + 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, movie', + 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV show', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', 'accessibility.mediaCardSeason' => ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}', 'accessibility.mediaCardWatched' => 'watched', @@ -7097,10 +7105,10 @@ extension on Translations { 'explore.stats.viewersAllTime' => ({required Object n}) => '${n} viewers', 'explore.stats.planning' => ({required Object n}) => '${n} planning to watch', 'explore.stats.favorited' => ({required Object n}) => '${n} favorites', - 'explore.stats.dropRate' => ({required Object percent}) => '${percent} dropped it', - 'explore.stats.comments' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} comment', other: '${n} comments', ), _ => null, } ?? switch (path) { + 'explore.stats.dropRate' => ({required Object percent}) => '${percent} dropped it', + 'explore.stats.comments' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} comment', other: '${n} comments', ), 'explore.stats.votes' => ({required Object n}) => '${n} votes', 'explore.stats.watching' => ({required Object n}) => '${n} watching it', 'explore.stats.completed' => ({required Object n}) => '${n} completed', @@ -7611,10 +7619,10 @@ extension on Translations { 'externalPlayer.title' => 'External Player', 'externalPlayer.useExternalPlayer' => 'Use External Player', 'externalPlayer.useExternalPlayerDescription' => 'Open videos in another app', - 'externalPlayer.selectPlayer' => 'Select Player', - 'externalPlayer.customPlayers' => 'Custom Players', _ => null, } ?? switch (path) { + 'externalPlayer.selectPlayer' => 'Select Player', + 'externalPlayer.customPlayers' => 'Custom Players', 'externalPlayer.systemDefault' => 'System Default', 'externalPlayer.addCustomPlayer' => 'Add Custom Player', 'externalPlayer.playerName' => 'Player Name', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 14cd935b..3a91d551 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Visa navigeringsfältets etiketter", "showNavBarLabelsDescription": "Visa textetiketter under navigeringsfältets ikoner", "startupSection": "Startsida", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Öppna med favoriter", "liveTvDefaultFavoritesDescription": "Visa endast favoritkanaler när Live-TV öppnas", "display": "Skärm", diff --git a/lib/i18n/tr.i18n.json b/lib/i18n/tr.i18n.json index 9ed18da5..34bf24ad 100644 --- a/lib/i18n/tr.i18n.json +++ b/lib/i18n/tr.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Gezinme Çubuğu Etiketlerini Göster", "showNavBarLabelsDescription": "Gezinme çubuğu simgelerinin altında metin etiketlerini göster", "startupSection": "Başlangıç Bölümü", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Canlı TV'de Favorileri Varsayılan Yap", "liveTvDefaultFavoritesDescription": "Canlı TV açıldığında yalnızca favori kanalları göster", "display": "Ekran", diff --git a/lib/i18n/uz.i18n.json b/lib/i18n/uz.i18n.json index f75bb381..5e8740e9 100644 --- a/lib/i18n/uz.i18n.json +++ b/lib/i18n/uz.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "Navigatsiya paneli matnlarini koʻrsatish", "showNavBarLabelsDescription": "Navigatsiya belgilarining ostida matnni koʻrsatish", "startupSection": "Boshlangʻich boʻlim", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "Jonli TV-da tanlangan kanallarni standart qilish", "liveTvDefaultFavoritesDescription": "Jonli TV ochilganda faqat tanlangan kanallarni koʻrsatish", "display": "Displey", diff --git a/lib/i18n/zh-Hant.i18n.json b/lib/i18n/zh-Hant.i18n.json index 65d60575..c849fe91 100644 --- a/lib/i18n/zh-Hant.i18n.json +++ b/lib/i18n/zh-Hant.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "顯示導覽列標籤", "showNavBarLabelsDescription": "在導覽列圖示下方顯示文字標籤", "startupSection": "啟動頁面", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "預設顯示最愛頻道", "liveTvDefaultFavoritesDescription": "開啟直播電視時僅顯示最愛頻道", "display": "顯示器", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index d2ecf128..ea709b2c 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -323,6 +323,8 @@ "showNavBarLabels": "显示导航栏标签", "showNavBarLabelsDescription": "在导航栏图标下方显示文字标签", "startupSection": "启动页面", + "showExploreTab": "", + "showExploreTabDescription": "", "liveTvDefaultFavorites": "默认显示收藏频道", "liveTvDefaultFavoritesDescription": "打开直播电视时仅显示收藏频道", "display": "显示", diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index b1e6aff6..ae90f674 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -7,6 +7,7 @@ import 'dart:io' show Platform, exit; export '../navigation/main_screen_scope.dart' show MainScreenFocusScope, MainScreenScopeAspect, SideNavigationBleedBuilder; +import 'package:flutter/foundation.dart' show ValueListenable; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show HardwareKeyboard, KeyDownEvent, KeyRepeatEvent, KeyUpEvent, LogicalKeyboardKey; @@ -246,6 +247,7 @@ class _MainScreenState extends State OfflineModeProvider? _offlineModeProvider; MultiServerProvider? _multiServerProvider; CatalogSourcesProvider? _catalogSourcesProvider; + ValueListenable? _showExploreTabListenable; RouteObserver>? _profileRouteObserver; bool _lastHasLiveTv = false; bool _lastHasExplore = false; @@ -347,10 +349,14 @@ class _MainScreenState extends State _lastHasLiveTv = false; } try { - _lastHasExplore = context.read().hasAnySource; + _lastHasExplore = context.read().hasAnySource && _showExploreTabSetting; } catch (_) { _lastHasExplore = false; } + // Re-evaluate Explore tab visibility when the appearance toggle flips + // mid-session; the catalog-sources listener covers source changes. + _showExploreTabListenable = SettingsService.instanceOrNull?.listenable(SettingsService.showExploreTab); + _showExploreTabListenable?.addListener(_handleCatalogSourcesChanged); _currentTab = _defaultTabForMode(_isOffline); _lastOnlineTabId = _isOffline ? null : NavigationTabId.discover; _autoSwitchedToDownloads = _isOffline && _currentTab == NavigationTabId.downloads; @@ -887,6 +893,7 @@ class _MainScreenState extends State _offlineModeProvider?.removeListener(_handleOfflineStatusChanged); _multiServerProvider?.removeListener(_handleLiveTvChanged); _catalogSourcesProvider?.removeListener(_handleCatalogSourcesChanged); + _showExploreTabListenable?.removeListener(_handleCatalogSourcesChanged); if (_bindingSettleListener != null) { _activeProfileForListener?.removeListener(_bindingSettleListener!); } @@ -1078,8 +1085,10 @@ class _MainScreenState extends State } } + bool get _showExploreTabSetting => SettingsService.instanceOrNull?.read(SettingsService.showExploreTab) ?? true; + void _handleCatalogSourcesChanged() { - final hasExplore = _catalogSourcesProvider?.hasAnySource ?? false; + final hasExplore = (_catalogSourcesProvider?.hasAnySource ?? false) && _showExploreTabSetting; if (hasExplore == _lastHasExplore) return; _lastHasExplore = hasExplore; diff --git a/lib/screens/settings/appearance_settings_screen.dart b/lib/screens/settings/appearance_settings_screen.dart index 7f60ccb1..041ab5c8 100644 --- a/lib/screens/settings/appearance_settings_screen.dart +++ b/lib/screens/settings/appearance_settings_screen.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../../i18n/app_locale_utils.dart'; import '../../i18n/strings.g.dart'; +import '../../providers/catalog_sources_provider.dart'; import '../../providers/multi_server_provider.dart'; import '../../providers/theme_provider.dart'; import '../../profiles/active_profile_provider.dart'; @@ -29,9 +30,12 @@ class AppearanceSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // Watched at build level so the tile can be excluded with a plain `if` — + // Watched at build level so the tiles can be excluded with a plain `if` — // a child that renders SizedBox.shrink() would corrupt the group corners. final hasMultipleProfiles = context.watch().hasMultipleProfiles; + // Nullable watch: hosts without the profile session scope (tests) simply + // never show the Explore toggle, mirroring the tab's own visibility. + final hasExplore = context.watch()?.hasAnySource ?? false; return SettingsPage( title: Text(t.settings.appearance), children: [ @@ -112,6 +116,13 @@ class AppearanceSettingsScreen extends StatelessWidget { title: t.settings.navigation, children: [ _startupSectionSelector(), + if (hasExplore) + SettingSwitchTile( + pref: SettingsService.showExploreTab, + icon: Symbols.explore_rounded, + title: t.settings.showExploreTab, + subtitle: t.settings.showExploreTabDescription, + ), if (Platform.isAndroid || PlatformDetector.isDesktopOS()) SettingSwitchTile( pref: SettingsService.forceTvMode, diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 42de9ad0..476d07c8 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -444,6 +444,11 @@ class SettingsService extends BaseSharedPreferencesService { values: NavigationTabId.values, defaultValue: NavigationTabId.discover, ); + + /// Whether the Explore tab (Plex Discover / tracker catalog rows) is shown + /// at all. UI-only: catalog sources stay connected so watchlist surfaces + /// keep working while the tab is hidden. + static const showExploreTab = BoolPref('show_explore_tab', defaultValue: true); static const alwaysKeepSidebarOpen = BoolPref('always_keep_sidebar_open'); static const showUnwatchedCount = BoolPref('show_unwatched_count', defaultValue: true); static const showEpisodeNumberOnCards = BoolPref('show_episode_number_on_cards', defaultValue: true); @@ -920,6 +925,7 @@ class SettingsService extends BaseSharedPreferencesService { autoPlayNextEpisode, useExoPlayer, startupSection, + showExploreTab, alwaysKeepSidebarOpen, showUnwatchedCount, showEpisodeNumberOnCards, diff --git a/lib/widgets/side_navigation_rail.dart b/lib/widgets/side_navigation_rail.dart index dabc5cf7..f9c37787 100644 --- a/lib/widgets/side_navigation_rail.dart +++ b/lib/widgets/side_navigation_rail.dart @@ -652,7 +652,7 @@ class SideNavigationRailState extends State with MountedSetS final hasLiveTv = context.watch().hasLiveTv; // Nullable watch: rail tests (and any host without the profile session // scope) simply never show the Explore item. - final hasExplore = context.watch()?.hasAnySource ?? false; + final hasExploreSource = context.watch()?.hasAnySource ?? false; // Nullable watch: rail tests (and any host without the profile session // scope) simply never show the Now Playing item. TV-only — it is the // way back into the now-playing screen there; desktop already has the @@ -660,14 +660,16 @@ class SideNavigationRailState extends State with MountedSetS final musicService = context.watch(); final nowPlayingTrack = widget.isOfflineMode || !PlatformDetector.isTV() ? null : musicService?.currentTrack; - // Listen to fullscreen + groupLibrariesByServer setting so the rail - // rebuilds when the user toggles "Group libraries by server" in Appearance. + // Listen to fullscreen + the groupLibrariesByServer / showExploreTab + // settings so the rail rebuilds when they are toggled in Appearance. return ListenableBuilder( listenable: Listenable.merge([ FullscreenStateManager(), SettingsService.instance.listenable(SettingsService.groupLibrariesByServer), + SettingsService.instance.listenable(SettingsService.showExploreTab), ]), builder: (context, _) { + final hasExplore = hasExploreSource && SettingsService.instance.read(SettingsService.showExploreTab); // Server grouping: only when multi-server AND the user-facing toggle is on. final groupByServerSetting = SettingsService.instance.read(SettingsService.groupLibrariesByServer); final showServerHeaders = serverIds.length > 1 && groupByServerSetting; diff --git a/test/widgets/side_navigation_rail_test.dart b/test/widgets/side_navigation_rail_test.dart index 158f3429..d4887b59 100644 --- a/test/widgets/side_navigation_rail_test.dart +++ b/test/widgets/side_navigation_rail_test.dart @@ -11,6 +11,7 @@ import 'package:plezy/media/media_backend.dart'; import 'package:plezy/media/media_kind.dart'; import 'package:plezy/media/media_library.dart'; import 'package:plezy/navigation/navigation_tabs.dart'; +import 'package:plezy/providers/catalog_sources_provider.dart'; import 'package:plezy/providers/hidden_libraries_provider.dart'; import 'package:plezy/providers/libraries_provider.dart'; import 'package:plezy/providers/multi_server_provider.dart'; @@ -25,6 +26,12 @@ import '../test_helpers/multi_server_fixtures.dart'; import '../test_helpers/prefs.dart'; import '../test_helpers/theme.dart'; +/// Minimal source-bearing stand-in: the rail only reads [hasAnySource]. +class _FakeCatalogSourcesProvider extends CatalogSourcesProvider { + @override + bool get hasAnySource => true; +} + MediaLibrary _library({ required String id, required String title, @@ -68,6 +75,7 @@ Future _pumpBasicRail( bool isSidebarFocused = false, bool alwaysExpanded = false, double? height, + CatalogSourcesProvider? catalogSources, }) async { await SettingsService.getInstance(); @@ -102,6 +110,7 @@ Future _pumpBasicRail( ChangeNotifierProvider.value(value: librariesProvider), ChangeNotifierProvider.value(value: hiddenLibrariesProvider), ChangeNotifierProvider.value(value: multiServerProvider), + if (catalogSources != null) ChangeNotifierProvider.value(value: catalogSources), ], child: MaterialApp( theme: ThemeData(extensions: const [testMonoTokens]), @@ -295,6 +304,22 @@ void main() { expect(targetRect.bottom, lessThanOrEqualTo(railRect.bottom)); }); + testWidgets('Explore item follows the showExploreTab appearance setting', (tester) async { + final catalogSources = _FakeCatalogSourcesProvider(); + addTearDown(catalogSources.dispose); + + await _pumpBasicRail(tester, alwaysExpanded: true, catalogSources: catalogSources); + expect(find.widgetWithText(NavigationRailItem, 'Explore'), findsOneWidget); + + await SettingsService.instance.write(SettingsService.showExploreTab, false); + await tester.pumpAndSettle(); + expect(find.widgetWithText(NavigationRailItem, 'Explore'), findsNothing); + + await SettingsService.instance.write(SettingsService.showExploreTab, true); + await tester.pumpAndSettle(); + expect(find.widgetWithText(NavigationRailItem, 'Explore'), findsOneWidget); + }); + testWidgets('reports interaction expansion for shell content push', (tester) async { await SettingsService.getInstance();