为什么:客户端此前完全没有账号概念(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 全过。
174 lines
4.2 KiB
Dart
174 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../core/data/models/drama.dart';
|
|
import '../../core/data/services/catalog_controller.dart';
|
|
import '../discover/discover_logic.dart';
|
|
import '../drama_detail/drama_detail_view.dart';
|
|
import '../mine/mine_logic.dart';
|
|
import '../player/player_view.dart';
|
|
|
|
class HomeLogic extends ChangeNotifier {
|
|
HomeLogic(List<Drama> dramas) : currentDrama = dramas.first;
|
|
|
|
final scaffoldKey = GlobalKey<ScaffoldState>();
|
|
Drama currentDrama;
|
|
int tab = 1;
|
|
int searchRequest = 0;
|
|
int sectionRequest = 0;
|
|
int resumeRequest = 0;
|
|
int backRequest = 0;
|
|
int mySection = 0;
|
|
bool drawerOpen = false;
|
|
bool detailOpen = false;
|
|
bool videoImmersive = false;
|
|
bool profile = false;
|
|
bool _disposed = false;
|
|
|
|
void notify() {
|
|
if (!_disposed) notifyListeners();
|
|
}
|
|
|
|
void select(
|
|
int nextTab, {
|
|
bool search = false,
|
|
bool watchlist = false,
|
|
bool resume = false,
|
|
int? mySection,
|
|
}) {
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
tab = nextTab;
|
|
if (search) searchRequest++;
|
|
if (watchlist || mySection != null) {
|
|
this.mySection = mySection ?? 1;
|
|
sectionRequest++;
|
|
}
|
|
if (resume) resumeRequest++;
|
|
notify();
|
|
}
|
|
|
|
void openMenu() {
|
|
drawerOpen = true;
|
|
notify();
|
|
scaffoldKey.currentState?.openDrawer();
|
|
}
|
|
|
|
void selectFromMenu(int nextTab, {int? mySection}) {
|
|
scaffoldKey.currentState?.closeDrawer();
|
|
select(nextTab, mySection: mySection);
|
|
}
|
|
|
|
Future<void> openCurrentSeries(BuildContext context) async {
|
|
detailOpen = true;
|
|
notify();
|
|
scaffoldKey.currentState?.closeDrawer();
|
|
await WidgetsBinding.instance.endOfFrame;
|
|
if (!context.mounted) return;
|
|
try {
|
|
await Navigator.of(context).push<void>(
|
|
MaterialPageRoute(builder: (_) => DramaDetailPage(drama: currentDrama)),
|
|
);
|
|
} finally {
|
|
if (context.mounted) {
|
|
detailOpen = false;
|
|
notify();
|
|
}
|
|
}
|
|
}
|
|
|
|
void openDrama(BuildContext context, Drama drama) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute<void>(builder: (_) => DramaDetailPage(drama: drama)),
|
|
);
|
|
}
|
|
|
|
Future<void> playDrama(BuildContext context, Drama drama) async {
|
|
// 列表卡片只有一集预览,进入播放前补齐完整剧集,否则选集列表是空的。
|
|
var playable = drama;
|
|
try {
|
|
playable = await context.read<CatalogController>().requireDetail(drama);
|
|
} catch (_) {
|
|
// 取详情失败就用卡片自带的那一集起播。
|
|
}
|
|
if (!context.mounted) return;
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => PlayerPage(drama: playable, initialIndex: 0),
|
|
),
|
|
);
|
|
}
|
|
|
|
void openProfile() {
|
|
profile = true;
|
|
scaffoldKey.currentState?.closeDrawer();
|
|
select(2, mySection: 0);
|
|
}
|
|
|
|
void openLibrary({int section = 1}) {
|
|
profile = false;
|
|
scaffoldKey.currentState?.closeDrawer();
|
|
select(2, mySection: section);
|
|
}
|
|
|
|
void discoverAction(BuildContext context, DiscoverPageAction action) {
|
|
switch (action) {
|
|
case DiscoverPageAction.home:
|
|
select(0);
|
|
case DiscoverPageAction.my:
|
|
select(2);
|
|
case DiscoverPageAction.watchlist:
|
|
openLibrary();
|
|
case DiscoverPageAction.resume:
|
|
select(0, resume: true);
|
|
case DiscoverPageAction.detail:
|
|
openDrama(context, currentDrama);
|
|
}
|
|
}
|
|
|
|
void myAction(MyPageAction action) {
|
|
switch (action) {
|
|
case MyPageAction.home:
|
|
select(0);
|
|
case MyPageAction.discover:
|
|
select(1, search: true);
|
|
case MyPageAction.resume:
|
|
select(0, resume: true);
|
|
}
|
|
}
|
|
|
|
void onSystemBack() {
|
|
if (drawerOpen) {
|
|
scaffoldKey.currentState?.closeDrawer();
|
|
} else if (videoImmersive) {
|
|
backRequest++;
|
|
notify();
|
|
} else if (tab != 1) {
|
|
select(1);
|
|
} else {
|
|
openMenu();
|
|
}
|
|
}
|
|
|
|
void onDrawerChanged(bool opened) {
|
|
if (drawerOpen == opened) return;
|
|
drawerOpen = opened;
|
|
notify();
|
|
}
|
|
|
|
void onDramaChanged(Drama drama) {
|
|
currentDrama = drama;
|
|
notify();
|
|
}
|
|
|
|
void onImmersiveChanged(bool value) {
|
|
videoImmersive = value;
|
|
notify();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_disposed = true;
|
|
super.dispose();
|
|
}
|
|
}
|