part of '../di_providers.dart'; typedef TmdbMediaKey = ({String tmdbId, String mediaType}); typedef TmdbSeasonKey = ({String tmdbId, int seasonNumber}); typedef _TmdbReady = ({TmdbRequestConfig config, String language}); final _tmdbReadyProvider = FutureProvider<_TmdbReady?>((ref) async { final settings = await ref.watch(tmdbSettingsProvider.future); if (!settings.canRequest) return null; if (settings.accessMode == TmdbAccessMode.smAccount) { final account = await ref.watch(smAccountProvider.future); if (!account.canUseDataPlane) return null; final notifier = ref.read(smAccountProvider.notifier); final token = await notifier.getAccessToken(); if (token == null) return null; return ( config: TmdbRequestConfig( apiBaseUrl: account.tmdbApiBaseUrl, imageBaseUrl: TmdbSettingsState.officialImageBaseUrl, accessToken: token, apiKey: '', onUnauthorized: () => notifier.getAccessToken(force: true), ), language: settings.language, ); } return ( config: TmdbRequestConfig( apiBaseUrl: settings.effectiveApiBaseUrl, imageBaseUrl: settings.effectiveImageBaseUrl, accessToken: settings.effectiveAccessToken, apiKey: settings.effectiveApiKey, ), language: settings.language, ); }); final _tmdbCacheScopeProvider = FutureProvider(( ref, ) async { final settings = await ref.watch(tmdbSettingsProvider.future); if (!settings.canRequest) return null; if (settings.accessMode == TmdbAccessMode.smAccount) { final account = await ref.watch(smAccountProvider.future); if (!account.hasActiveSession) return null; return DiscoverPageCacheScope( apiBaseUrl: account.tmdbApiBaseUrl, imageBaseUrl: TmdbSettingsState.officialImageBaseUrl, language: settings.language, ); } return DiscoverPageCacheScope( apiBaseUrl: settings.effectiveApiBaseUrl, imageBaseUrl: settings.effectiveImageBaseUrl, language: settings.language, ); }); final tmdbEnrichedDetailProvider = FutureProvider.family((ref, key) async { final ready = await ref.watch(_tmdbReadyProvider.future); if (ready == null || key.tmdbId.isEmpty) return null; return ref .read(tmdbGatewayProvider) .getEnrichedDetail( ready.config, key.tmdbId, key.mediaType, language: ready.language, ); }); final tmdbImagesProvider = Provider.family(( ref, key, ) { return ref.watch(tmdbEnrichedDetailProvider(key)).value?.images; }); final tmdbMediaDetailProvider = Provider.family( (ref, key) { return ref.watch(tmdbEnrichedDetailProvider(key)).value?.detail; }, ); final tmdbSeasonDetailProvider = FutureProvider.family((ref, key) async { final ready = await ref.watch(_tmdbReadyProvider.future); if (ready == null || key.tmdbId.isEmpty) return null; return ref .read(tmdbGatewayProvider) .getSeasonDetail( ready.config, key.tmdbId, key.seasonNumber, language: ready.language, ); }); enum _TmdbRecommendationRowKind { trendingDay, trendingWeek, popularMovies, popularTv, airingTodayTv, topRatedMovies, } final _tmdbRecommendationRowProvider = AsyncNotifierProvider.family< _TmdbRecommendationRowNotifier, TmdbRecommendationsRes?, _TmdbRecommendationRowKind >(_TmdbRecommendationRowNotifier.new); final tmdbTrendingDayProvider = _tmdbRecommendationRowProvider( _TmdbRecommendationRowKind.trendingDay, ); final tmdbTrendingWeekProvider = _tmdbRecommendationRowProvider( _TmdbRecommendationRowKind.trendingWeek, ); final tmdbPopularMoviesProvider = _tmdbRecommendationRowProvider( _TmdbRecommendationRowKind.popularMovies, ); final tmdbPopularTvProvider = _tmdbRecommendationRowProvider( _TmdbRecommendationRowKind.popularTv, ); final tmdbAiringTodayTvProvider = _tmdbRecommendationRowProvider( _TmdbRecommendationRowKind.airingTodayTv, ); final tmdbTopRatedMoviesProvider = _tmdbRecommendationRowProvider( _TmdbRecommendationRowKind.topRatedMovies, ); typedef TmdbDiscoverKey = ({ String mediaType, String sortBy, String? withGenres, String? voteCountGte, }); final tmdbDiscoverProvider = AsyncNotifierProvider.family< _TmdbDiscoverRowNotifier, TmdbRecommendationsRes?, TmdbDiscoverKey >(_TmdbDiscoverRowNotifier.new); class _TmdbRecommendationRowNotifier extends AsyncNotifier { final _TmdbRecommendationRowKind arg; _TmdbRecommendationRowNotifier(this.arg); @override Future build() async { final rowKey = _rowKeyForKind(arg); final scope = await ref.watch(_tmdbCacheScopeProvider.future); if (scope == null) return null; final cached = await _readDiscoverRowCacheByScope(ref, scope, rowKey); if (cached != null) { unawaited(_refreshFromNetwork(arg, rowKey)); return cached; } final ready = await ref.watch(_tmdbReadyProvider.future); if (ready == null) return null; return _fetchAndCache(arg, ready, rowKey); } Future _refreshFromNetwork( _TmdbRecommendationRowKind kind, String rowKey, ) async { try { final ready = await ref.read(_tmdbReadyProvider.future); if (ready == null) return; final fresh = await _fetchAndCache(kind, ready, rowKey); if (fresh != null) { state = AsyncValue.data(fresh); } } catch (e) { AppLogger.warn('TMDB', 'refresh discover row $rowKey failed', e); } } Future _fetchAndCache( _TmdbRecommendationRowKind kind, _TmdbReady ready, String rowKey, ) async { final fresh = await _fetchRow(kind, ready); if (fresh != null) { await _writeDiscoverRowCache(ref, ready, rowKey, fresh); } return fresh; } Future _fetchRow( _TmdbRecommendationRowKind kind, _TmdbReady ready, ) { final gateway = ref.read(tmdbGatewayProvider); return switch (kind) { _TmdbRecommendationRowKind.trendingDay => gateway.getTrending( ready.config, language: ready.language, mediaType: 'all', timeWindow: 'day', ), _TmdbRecommendationRowKind.trendingWeek => gateway.getTrending( ready.config, language: ready.language, mediaType: 'all', timeWindow: 'week', ), _TmdbRecommendationRowKind.popularMovies => gateway.getPopularMovies( ready.config, language: ready.language, ), _TmdbRecommendationRowKind.popularTv => gateway.getPopularTv( ready.config, language: ready.language, ), _TmdbRecommendationRowKind.airingTodayTv => gateway.getAiringTodayTv( ready.config, language: ready.language, ), _TmdbRecommendationRowKind.topRatedMovies => gateway.getTopRatedMovies( ready.config, language: ready.language, ), }; } } class _TmdbDiscoverRowNotifier extends AsyncNotifier { final TmdbDiscoverKey arg; _TmdbDiscoverRowNotifier(this.arg); @override Future build() async { final rowKey = _rowKeyForDiscover(arg); final scope = await ref.watch(_tmdbCacheScopeProvider.future); if (scope == null) return null; final cached = await _readDiscoverRowCacheByScope(ref, scope, rowKey); if (cached != null) { unawaited(_refreshFromNetwork(arg, rowKey)); return cached; } final ready = await ref.watch(_tmdbReadyProvider.future); if (ready == null) return null; return _fetchAndCache(arg, ready, rowKey); } Future _refreshFromNetwork(TmdbDiscoverKey key, String rowKey) async { try { final ready = await ref.read(_tmdbReadyProvider.future); if (ready == null) return; final fresh = await _fetchAndCache(key, ready, rowKey); if (fresh != null) { state = AsyncValue.data(fresh); } } catch (e) { AppLogger.warn('TMDB', 'refresh discover row $rowKey failed', e); } } Future _fetchAndCache( TmdbDiscoverKey key, _TmdbReady ready, String rowKey, ) async { final fresh = await ref .read(tmdbGatewayProvider) .getDiscover( ready.config, key.mediaType, language: ready.language, sortBy: key.sortBy, withGenres: key.withGenres, voteCountGte: key.voteCountGte, ); if (fresh != null) { await _writeDiscoverRowCache(ref, ready, rowKey, fresh); } return fresh; } } DiscoverPageCacheScope _cacheScopeForReady(_TmdbReady ready) { return DiscoverPageCacheScope( apiBaseUrl: ready.config.apiBaseUrl, imageBaseUrl: ready.config.imageBaseUrl, language: ready.language, ); } Future _readDiscoverRowCacheByScope( Ref ref, DiscoverPageCacheScope scope, String rowKey, ) async { final store = await ref.read(discoverPageCacheStoreProvider.future); final entry = await store.readRow(scope, rowKey); return entry?.data; } Future _writeDiscoverRowCache( Ref ref, _TmdbReady ready, String rowKey, TmdbRecommendationsRes data, ) async { final store = await ref.read(discoverPageCacheStoreProvider.future); await store.writeRow(_cacheScopeForReady(ready), rowKey, data); } String _rowKeyForKind(_TmdbRecommendationRowKind kind) { return switch (kind) { _TmdbRecommendationRowKind.trendingDay => 'trending:day', _TmdbRecommendationRowKind.trendingWeek => 'trending:week', _TmdbRecommendationRowKind.popularMovies => 'popular:movie', _TmdbRecommendationRowKind.popularTv => 'popular:tv', _TmdbRecommendationRowKind.airingTodayTv => 'airing_today:tv', _TmdbRecommendationRowKind.topRatedMovies => 'top_rated:movie', }; } String _rowKeyForDiscover(TmdbDiscoverKey key) { return [ 'discover', key.mediaType, key.sortBy, key.withGenres ?? '', key.voteCountGte ?? '', ].join(':'); }