feat(explore): Explore tab with catalog detail, search, and Seerr requests

This commit is contained in:
edde746
2026-07-10 07:10:15 +02:00
parent f94b8c6586
commit 04daf89a20
25 changed files with 3403 additions and 250 deletions
+18 -5
View File
@@ -6,7 +6,7 @@ import '../i18n/strings.g.dart';
import '../utils/platform_detector.dart';
/// Navigation tab identifiers
enum NavigationTabId { discover, libraries, liveTv, search, downloads, settings }
enum NavigationTabId { discover, explore, libraries, liveTv, search, downloads, settings }
/// Represents a navigation tab with its configuration
class NavigationTab {
@@ -22,16 +22,21 @@ class NavigationTab {
}
/// Get the index for a tab ID in the visible tabs list
static int indexFor(NavigationTabId id, {required bool isOffline, bool hasLiveTv = false}) {
final tabs = getVisibleTabs(isOffline: isOffline, hasLiveTv: hasLiveTv);
static int indexFor(NavigationTabId id, {required bool isOffline, bool hasLiveTv = false, bool hasExplore = false}) {
final tabs = getVisibleTabs(isOffline: isOffline, hasLiveTv: hasLiveTv, hasExplore: hasExplore);
return tabs.indexWhere((tab) => tab.id == id);
}
/// Get tabs filtered by offline mode and feature availability
static List<NavigationTab> getVisibleTabs({required bool isOffline, bool hasLiveTv = false}) {
static List<NavigationTab> getVisibleTabs({
required bool isOffline,
bool hasLiveTv = false,
bool hasExplore = false,
}) {
return allNavigationTabs.where((tab) {
if (isOffline && tab.onlineOnly) return false;
if (tab.id == NavigationTabId.liveTv && !hasLiveTv) return false;
if (tab.id == NavigationTabId.explore && !hasExplore) return false;
if (tab.id == NavigationTabId.downloads && PlatformDetector.isAppleTV()) return false;
return true;
}).toList();
@@ -45,9 +50,10 @@ class NavigationTab {
static NavigationTabId resolveDefaultTab({
required bool isOffline,
required bool hasLiveTv,
bool hasExplore = false,
required NavigationTabId? preferredStartup,
}) {
final tabs = getVisibleTabs(isOffline: isOffline, hasLiveTv: hasLiveTv);
final tabs = getVisibleTabs(isOffline: isOffline, hasLiveTv: hasLiveTv, hasExplore: hasExplore);
if (isOffline && tabs.any((t) => t.id == NavigationTabId.downloads)) {
return NavigationTabId.downloads;
}
@@ -60,6 +66,7 @@ class NavigationTab {
// Label getters (must be top-level for const constructor)
String _getHomeLabel() => t.common.home;
String _getExploreLabel() => t.navigation.explore;
String _getLibrariesLabel() => t.navigation.libraries;
String _getLiveTvLabel() => t.navigation.liveTv;
String _getSearchLabel() => t.common.search;
@@ -76,6 +83,12 @@ const allNavigationTabs = [
getLabel: _getLibrariesLabel,
),
NavigationTab(id: NavigationTabId.liveTv, onlineOnly: true, icon: Symbols.live_tv_rounded, getLabel: _getLiveTvLabel),
NavigationTab(
id: NavigationTabId.explore,
onlineOnly: true,
icon: Symbols.explore_rounded,
getLabel: _getExploreLabel,
),
NavigationTab(id: NavigationTabId.search, onlineOnly: true, icon: Symbols.search_rounded, getLabel: _getSearchLabel),
NavigationTab(
id: NavigationTabId.downloads,
@@ -10,18 +10,22 @@ import '../media/media_server_client.dart';
import '../profiles/active_profile_provider.dart';
import '../profiles/plex_home_service.dart';
import '../profiles/profile_connection_registry.dart';
import '../providers/catalog_sources_provider.dart';
import '../providers/companion_remote_provider.dart';
import '../providers/discover_provider.dart';
import '../providers/explore_provider.dart';
import '../providers/hidden_libraries_provider.dart';
import '../providers/libraries_provider.dart';
import '../providers/multi_server_provider.dart';
import '../providers/playback_state_provider.dart';
import '../providers/trakt_account_provider.dart';
import '../providers/seerr_account_provider.dart';
import '../providers/trackers_provider.dart';
import '../providers/watch_state_store.dart';
import '../database/app_database.dart';
import '../screens/main_screen.dart';
import '../services/api_cache.dart';
import '../services/catalog/catalog_library_matcher.dart';
import '../services/music/music_playback_service.dart';
import '../services/music/music_playback_service_impl.dart';
import '../services/offline_watch_sync_service.dart';
@@ -143,6 +147,50 @@ class _ProfileSessionScreenState extends State<ProfileSessionScreen> {
return provider;
},
),
ChangeNotifierProvider(
create: (context) {
final provider = SeerrAccountProvider();
provider.bindPlexTokenSupplier(
buildSeerrPlexTokenSupplier(
activeProfile: context.read<ActiveProfileProvider>(),
connections: context.read<ConnectionRegistry>(),
profileConnections: context.read<ProfileConnectionRegistry>(),
),
);
unawaited(
provider.onActiveProfileChanged(activeId).catchError((Object e, StackTrace s) {
appLogger.w('Seerr profile hydrate failed', error: e, stackTrace: s);
}),
);
return provider;
},
),
ChangeNotifierProxyProvider3<
TraktAccountProvider,
TrackersProvider,
SeerrAccountProvider,
CatalogSourcesProvider
>(
create: (context) {
final provider = CatalogSourcesProvider();
unawaited(
provider.onActiveProfileChanged(activeId).catchError((Object e, StackTrace s) {
appLogger.w('Catalog sources profile hydrate failed', error: e, stackTrace: s);
}),
);
return provider;
},
update: (_, trakt, trackers, seerr, previous) {
final provider = previous ?? CatalogSourcesProvider();
provider.update(trakt, trackers, seerr);
return provider;
},
),
ChangeNotifierProvider(
create: (context) => ExploreProvider(context.read<CatalogSourcesProvider>()),
lazy: true,
),
Provider(create: (context) => CatalogLibraryMatcher(context.read<MultiServerProvider>()), lazy: true),
ChangeNotifierProvider(
create: (context) =>
HiddenLibrariesProvider(storageService: context.read<StorageService>(), profileId: activeId),