Files
ReelShort/lib/app.dart
T
Vicky e1b98df509 feat(client): C 端 1-2 账号链路(邮箱登录/注册 + 令牌与 401 + 匿名/登录分叉)
为什么:客户端此前完全没有账号概念(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 全过。
2026-10-15 17:53:35 +08:00

112 lines
4.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(),
),
),
);
}
}