344 lines
13 KiB
Dart
344 lines
13 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'core/config/app_locale.dart';
|
|
import 'core/data/remote/account_api.dart';
|
|
import 'core/data/remote/api_client.dart';
|
|
import 'core/data/remote/auth_api.dart';
|
|
import 'core/data/remote/catalog_api.dart';
|
|
import 'core/data/remote/category_api.dart';
|
|
import 'core/data/remote/comment_api.dart';
|
|
import 'core/data/remote/commerce_api.dart';
|
|
import 'core/data/remote/config_api.dart';
|
|
import 'core/data/remote/device_profile.dart';
|
|
import 'core/data/remote/download_api.dart';
|
|
import 'core/data/remote/interaction_api.dart';
|
|
import 'core/data/remote/library_api.dart';
|
|
import 'core/data/remote/notice_api.dart';
|
|
import 'core/data/remote/playback_api.dart';
|
|
import 'core/data/remote/watch_api.dart';
|
|
import 'core/data/repositories/catalog_repository.dart';
|
|
import 'core/data/repositories/account_repository.dart';
|
|
import 'core/data/repositories/category_repository.dart';
|
|
import 'core/data/repositories/comment_repository.dart';
|
|
import 'core/data/repositories/commerce_repository.dart';
|
|
import 'core/data/repositories/download_repository.dart';
|
|
import 'core/data/repositories/favorite_repository.dart';
|
|
import 'core/data/repositories/interaction_repository.dart';
|
|
import 'core/data/repositories/notice_repository.dart';
|
|
import 'core/data/repositories/playback_repository.dart';
|
|
import 'core/data/repositories/playlist_repository.dart';
|
|
import 'core/data/repositories/search_repository.dart';
|
|
import 'core/data/repositories/store_repository.dart';
|
|
import 'core/data/repositories/watch_history_repository.dart';
|
|
import 'core/data/services/app_config_controller.dart';
|
|
import 'core/data/services/auth_controller.dart';
|
|
import 'core/data/services/auth_session_store.dart';
|
|
import 'core/data/services/catalog_controller.dart';
|
|
import 'core/data/services/category_controller.dart';
|
|
import 'core/data/services/download_controller.dart';
|
|
import 'core/data/services/favorite_controller.dart';
|
|
import 'core/data/services/interaction_controller.dart';
|
|
import 'core/data/services/locale_controller.dart';
|
|
import 'core/data/services/playlist_controller.dart';
|
|
import 'core/data/services/event_tracker.dart';
|
|
import 'core/data/services/playback_position_cache.dart';
|
|
import 'core/data/services/playback_session.dart';
|
|
import 'core/data/services/unlocked_episode_registry.dart';
|
|
import 'core/data/services/video_player_port.dart';
|
|
import 'core/data/services/wallet_controller.dart';
|
|
import 'core/navigation/app_route_observer.dart';
|
|
import 'core/platform/haptics.dart';
|
|
import 'core/theme/app_theme.dart';
|
|
import 'core/theme/app_colors.dart';
|
|
import 'l10n/app_localizations.dart';
|
|
import 'pages/home/home_view.dart';
|
|
|
|
/// 全局系统栏样式:深色底 + 浅色图标,关闭 Android 对比遮罩。
|
|
final SystemUiOverlayStyle appSystemOverlayStyle = SystemUiOverlayStyle.light
|
|
.copyWith(
|
|
statusBarColor: AppColors.transparent,
|
|
statusBarIconBrightness: Brightness.light,
|
|
statusBarBrightness: Brightness.dark,
|
|
systemNavigationBarColor: AppColors.ink,
|
|
systemNavigationBarDividerColor: AppColors.transparent,
|
|
systemNavigationBarIconBrightness: Brightness.light,
|
|
systemNavigationBarContrastEnforced: false,
|
|
);
|
|
|
|
class LumaDramaApp extends StatefulWidget {
|
|
const LumaDramaApp({
|
|
super.key,
|
|
this.playerFactory = VideoPlayerPort.new,
|
|
this.catalogRepository,
|
|
this.categoryRepository,
|
|
this.downloadRepository,
|
|
this.watchHistoryRepository,
|
|
this.favoriteRepository,
|
|
this.playbackRepository,
|
|
this.playbackPositionCache,
|
|
this.commentRepository,
|
|
this.interactionRepository,
|
|
this.noticeRepository,
|
|
this.accountRepository,
|
|
this.commerceRepository,
|
|
this.storeRepository,
|
|
this.searchRepository,
|
|
this.playlistRepository,
|
|
this.haptics = const FlutterHaptics(),
|
|
});
|
|
|
|
final PlayerFactory playerFactory;
|
|
|
|
/// 测试注入用;生产走 HTTP 仓库。
|
|
final CatalogRepository? catalogRepository;
|
|
final CategoryRepository? categoryRepository;
|
|
final DownloadRepository? downloadRepository;
|
|
final WatchHistoryRepository? watchHistoryRepository;
|
|
final FavoriteRepository? favoriteRepository;
|
|
final PlaybackRepository? playbackRepository;
|
|
final PlaybackPositionCache? playbackPositionCache;
|
|
final CommentRepository? commentRepository;
|
|
final InteractionRepository? interactionRepository;
|
|
final NoticeRepository? noticeRepository;
|
|
final AccountRepository? accountRepository;
|
|
final CommerceRepository? commerceRepository;
|
|
final StoreRepository? storeRepository;
|
|
final SearchRepository? searchRepository;
|
|
final PlaylistRepository? playlistRepository;
|
|
final Haptics haptics;
|
|
|
|
@override
|
|
State<LumaDramaApp> createState() => _LumaDramaAppState();
|
|
}
|
|
|
|
class _LumaDramaAppState extends State<LumaDramaApp> {
|
|
late final AuthSessionStore _sessionStore = AuthSessionStore();
|
|
|
|
/// 全 App 共用一个 ApiClient:这样目录、账号、后续的互动/进度请求
|
|
/// 都自动带 `Authorization`、`X-Device-Id` 与平台头,401 的刷新重试也只有一处实现。
|
|
late final ApiClient _apiClient = ApiClient(
|
|
accessTokenReader: () => _auth.accessToken,
|
|
tokenRefresher: () => _auth.refresh(),
|
|
onUnauthorized: () => _auth.clear(),
|
|
deviceIdReader: _sessionStore.deviceId,
|
|
platformReader: clientPlatform,
|
|
);
|
|
|
|
late final AuthController _auth = AuthController(
|
|
AuthApi(_apiClient),
|
|
_sessionStore,
|
|
);
|
|
|
|
late final CatalogApi _catalogApi = CatalogApi(_apiClient);
|
|
|
|
late final CatalogRepository _catalogRepository =
|
|
widget.catalogRepository ?? HttpCatalogRepository(_catalogApi);
|
|
|
|
late final CategoryRepository _categoryRepository =
|
|
widget.categoryRepository ??
|
|
HttpCategoryRepository(CategoryApi(_apiClient), _catalogRepository);
|
|
|
|
late final WatchHistoryRepository _watchHistory =
|
|
widget.watchHistoryRepository ??
|
|
HttpWatchHistoryRepository(WatchApi(_apiClient));
|
|
|
|
late final FavoriteRepository _favorites =
|
|
widget.favoriteRepository ??
|
|
HttpFavoriteRepository(LibraryApi(_apiClient));
|
|
|
|
late final PlaybackRepository _playback =
|
|
widget.playbackRepository ??
|
|
HttpPlaybackRepository(PlaybackApi(_apiClient));
|
|
|
|
late final PlaybackPositionCache _playbackPositionCache =
|
|
widget.playbackPositionCache ?? PlaybackPositionCache();
|
|
|
|
late final FavoriteController _favoriteController = FavoriteController(
|
|
_favorites,
|
|
);
|
|
|
|
late final DownloadRepository _downloadRepository =
|
|
widget.downloadRepository ??
|
|
HttpDownloadRepository(DownloadApi(_apiClient));
|
|
|
|
late final DownloadController _downloadController = DownloadController(
|
|
_downloadRepository,
|
|
);
|
|
|
|
late final CommerceRepository _commerceRepository =
|
|
widget.commerceRepository ??
|
|
HttpCommerceRepository(CommerceApi(_apiClient));
|
|
|
|
late final WalletController _walletController = WalletController(
|
|
_commerceRepository,
|
|
);
|
|
|
|
late final SearchRepository _searchRepository =
|
|
widget.searchRepository ?? HttpSearchRepository(CatalogApi(_apiClient));
|
|
|
|
/// 测试注入了目录仓库时不打剧单接口,剧单页继续用本地分区。
|
|
late final PlaylistRepository? _playlistRepository =
|
|
widget.playlistRepository ??
|
|
(widget.catalogRepository == null
|
|
? HttpPlaylistRepository(CatalogApi(_apiClient))
|
|
: null);
|
|
|
|
late final StoreRepository _storeRepository =
|
|
widget.storeRepository ?? HttpStoreRepository(CommerceApi(_apiClient));
|
|
|
|
late final CommentRepository _commentRepository =
|
|
widget.commentRepository ?? HttpCommentRepository(CommentApi(_apiClient));
|
|
|
|
late final InteractionRepository _interactionRepository =
|
|
widget.interactionRepository ??
|
|
HttpInteractionRepository(InteractionApi(_apiClient));
|
|
|
|
late final NoticeRepository _noticeRepository =
|
|
widget.noticeRepository ?? HttpNoticeRepository(NoticeApi(_apiClient));
|
|
|
|
late final AccountRepository _accountRepository =
|
|
widget.accountRepository ?? HttpAccountRepository(AccountApi(_apiClient));
|
|
|
|
late final InteractionController _interactionController =
|
|
InteractionController(_interactionRepository);
|
|
|
|
late final LocaleController _localeController = LocaleController();
|
|
|
|
/// 后台下发的启动配置(登录页背景等);拉不到就用本地默认。
|
|
late final AppConfigController _appConfigController = AppConfigController(
|
|
ConfigApi(_apiClient),
|
|
);
|
|
|
|
/// 登录期内共享的「已解锁收费集」:播放页进出、feed 与全屏切换都要沿用。
|
|
late final UnlockedEpisodeRegistry _unlockedEpisodes =
|
|
UnlockedEpisodeRegistry();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 恢复本地令牌与设备号;未登录也能正常浏览,不阻塞首屏
|
|
_auth.load();
|
|
unawaited(_localeController.load());
|
|
unawaited(_appConfigController.load());
|
|
// 登录/登出时同步收藏状态:收藏属于账号,不能跨账号串联
|
|
_auth.addListener(_onAuthChanged);
|
|
unawaited(_favoriteController.load());
|
|
if (_auth.isSignedIn) unawaited(_walletController.load());
|
|
unawaited(_downloadController.restore());
|
|
}
|
|
|
|
bool _wasSignedIn = false;
|
|
|
|
void _onAuthChanged() {
|
|
if (_auth.isSignedIn == _wasSignedIn) return;
|
|
_wasSignedIn = _auth.isSignedIn;
|
|
_interactionController.clear();
|
|
// 解锁结论属于账号:换人登录后不能沿用上一个人的可播判断。
|
|
_unlockedEpisodes.clear();
|
|
if (_auth.isSignedIn) {
|
|
unawaited(_favoriteController.load());
|
|
unawaited(_walletController.load());
|
|
} else {
|
|
_favoriteController.clear();
|
|
unawaited(_favoriteController.load());
|
|
_downloadController.reset();
|
|
_walletController.reset();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_auth.removeListener(_onAuthChanged);
|
|
_favoriteController.dispose();
|
|
_downloadController.dispose();
|
|
_walletController.dispose();
|
|
_interactionController.dispose();
|
|
_localeController.dispose();
|
|
_appConfigController.dispose();
|
|
_apiClient.close();
|
|
super.dispose();
|
|
}
|
|
|
|
CategoryController _createCategories() {
|
|
final controller = CategoryController(_categoryRepository);
|
|
if (widget.catalogRepository == null) {
|
|
final api = CategoryApi(_apiClient);
|
|
controller.dramaPageLoader = api.dramas;
|
|
controller.rankingLoader = api.ranking;
|
|
}
|
|
controller.load();
|
|
return controller;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final playlists = _playlistRepository;
|
|
return MultiProvider(
|
|
providers: [
|
|
Provider(create: (_) => PlaybackCoordinator()),
|
|
Provider<EventTracker>(create: (_) => EventTracker(_apiClient)),
|
|
Provider<PlayerFactory>(create: (_) => widget.playerFactory),
|
|
Provider<CatalogRepository>.value(value: _catalogRepository),
|
|
if (widget.catalogRepository == null)
|
|
Provider<CatalogApi>.value(value: _catalogApi),
|
|
Provider<PlaybackRepository>.value(value: _playback),
|
|
Provider<PlaybackPositionCache>.value(value: _playbackPositionCache),
|
|
Provider<UnlockedEpisodeRegistry>.value(value: _unlockedEpisodes),
|
|
Provider<WatchHistoryRepository>.value(value: _watchHistory),
|
|
Provider<FavoriteRepository>.value(value: _favorites),
|
|
Provider<CommentRepository>.value(value: _commentRepository),
|
|
Provider<InteractionRepository>.value(value: _interactionRepository),
|
|
Provider<NoticeRepository>.value(value: _noticeRepository),
|
|
Provider<AccountRepository>.value(value: _accountRepository),
|
|
Provider<CommerceRepository>.value(value: _commerceRepository),
|
|
Provider<StoreRepository>.value(value: _storeRepository),
|
|
Provider<SearchRepository>.value(value: _searchRepository),
|
|
Provider<Haptics>.value(value: widget.haptics),
|
|
ChangeNotifierProvider(
|
|
create: (_) {
|
|
final catalog = CatalogController(_catalogRepository);
|
|
if (widget.catalogRepository != null) catalog.load();
|
|
return catalog;
|
|
},
|
|
),
|
|
ChangeNotifierProvider(create: (_) => _createCategories()),
|
|
if (playlists != null)
|
|
ChangeNotifierProvider(create: (_) => PlaylistController(playlists)),
|
|
ChangeNotifierProvider.value(value: _favoriteController),
|
|
ChangeNotifierProvider.value(value: _downloadController),
|
|
ChangeNotifierProvider.value(value: _walletController),
|
|
ChangeNotifierProvider.value(value: _interactionController),
|
|
ChangeNotifierProvider.value(value: _localeController),
|
|
ChangeNotifierProvider.value(value: _appConfigController),
|
|
ChangeNotifierProvider.value(value: _auth),
|
|
],
|
|
child: Consumer<LocaleController>(
|
|
builder: (context, localeController, _) => MaterialApp(
|
|
locale: localeController.locale,
|
|
onGenerateTitle: (context) => AppLocalizations.of(context)!.appName,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
navigatorObservers: [appRouteObserver],
|
|
localeResolutionCallback: (deviceLocale, _) =>
|
|
resolveAppLocale(deviceLocale),
|
|
debugShowCheckedModeBanner: false,
|
|
themeMode: ThemeMode.dark,
|
|
theme: AppTheme.dark(),
|
|
darkTheme: AppTheme.dark(),
|
|
builder: (context, child) => AnnotatedRegion<SystemUiOverlayStyle>(
|
|
value: appSystemOverlayStyle,
|
|
child: child ?? const SizedBox.shrink(),
|
|
),
|
|
home: const HomePage(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|