为什么:客户端此前完全没有账号概念(mine 页的"登录"只是本地 demo 开关), 而受限动作(收藏/评论)要对到具体账号。本批把账号链路接上,为后续互动、进度同步、下载打底。 - AuthApi + AuthController + AuthSessionStore:注册/登录/登出/刷新,令牌与设备号存 shared_preferences (用 SharedPreferencesAsync,与 engagement 仓库同一套 API) - ApiClient:统一带 Authorization 与 X-Device-Id;401 自动刷新一次并重试,刷新失败清登录态走 onUnauthorized - 登录/注册页(pages/auth,沿用 binding/logic/view 与现有视觉):邮箱格式、密码 8-64 位含字母数字本地先校验, 错误提示走 l10n;登录/注册一键切换 - 匿名/登录分叉:首页、发现、详情保持匿名可看;收藏/点赞等受限动作走 ensureSignedIn() 弹底部提示引导登录 - mine 页展示真实账号(昵称 + uid),拿不到才回落到演示文案 - 测试:新增 auth 控制器(注册落盘/401 刷新重试/刷新失败清会话/登出)与登录页(校验/切换)用例; widget_test 改为预置本地会话(账号不再是本地开关);修掉 playback 用例对 host 大小写的环境依赖断言 门禁:flutter analyze 0 问题;flutter test 148 全过。
112 lines
4.0 KiB
Dart
112 lines
4.0 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
|
||
import 'core/config/locale_controller.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/repositories/catalog_repository.dart';
|
||
import 'core/data/repositories/engagement_repository.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/engagement_controller.dart';
|
||
import 'core/data/services/playback_session.dart';
|
||
import 'core/data/services/video_player_port.dart';
|
||
import 'l10n/app_localizations.dart';
|
||
import 'pages/home/home_view.dart';
|
||
|
||
class LumaDramaApp extends StatefulWidget {
|
||
const LumaDramaApp({
|
||
super.key,
|
||
this.playerFactory = VideoPlayerPort.new,
|
||
this.catalogRepository,
|
||
});
|
||
|
||
final PlayerFactory playerFactory;
|
||
|
||
/// 测试注入用;生产走 HTTP 仓库。
|
||
final CatalogRepository? catalogRepository;
|
||
|
||
@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,
|
||
);
|
||
|
||
late final AuthController _auth = AuthController(AuthApi(_apiClient), _sessionStore);
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
// 恢复本地令牌与设备号;未登录也能正常浏览,不阻塞首屏
|
||
_auth.load();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MultiProvider(
|
||
providers: [
|
||
Provider(create: (_) => PlaybackCoordinator()),
|
||
Provider<PlayerFactory>(create: (_) => widget.playerFactory),
|
||
ChangeNotifierProvider(
|
||
create: (_) => CatalogController(
|
||
widget.catalogRepository ?? HttpCatalogRepository(CatalogApi(_apiClient)),
|
||
)..load(),
|
||
),
|
||
ChangeNotifierProvider(create: (_) => LocaleController()..load()),
|
||
ChangeNotifierProvider(
|
||
create: (_) =>
|
||
EngagementController(SharedPreferencesEngagementRepository())
|
||
..load(),
|
||
),
|
||
ChangeNotifierProvider.value(value: _auth),
|
||
],
|
||
child: Consumer<LocaleController>(
|
||
builder: (context, locale, _) => MaterialApp(
|
||
onGenerateTitle: (context) => AppLocalizations.of(context)!.appName,
|
||
locale: locale.locale,
|
||
supportedLocales: AppLocalizations.supportedLocales,
|
||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||
localeResolutionCallback: (deviceLocale, supported) {
|
||
final resolved = locale.locale ?? deviceLocale;
|
||
if (resolved == null) return supported.first;
|
||
for (final item in supported) {
|
||
if (item.languageCode == resolved.languageCode) return item;
|
||
}
|
||
return supported.first;
|
||
},
|
||
debugShowCheckedModeBanner: false,
|
||
themeMode: ThemeMode.system,
|
||
theme: ThemeData(
|
||
useMaterial3: true,
|
||
colorScheme: ColorScheme.fromSeed(
|
||
seedColor: const Color(0xFF2F7BFF),
|
||
),
|
||
scaffoldBackgroundColor: const Color(0xFFF8F7FC),
|
||
),
|
||
darkTheme: ThemeData(
|
||
useMaterial3: true,
|
||
colorScheme: ColorScheme.fromSeed(
|
||
seedColor: const Color(0xFF2F7BFF),
|
||
brightness: Brightness.dark,
|
||
),
|
||
scaffoldBackgroundColor: const Color(0xFF101019),
|
||
),
|
||
home: const HomePage(),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|