diff --git a/android/app/src/main/res/drawable-hdpi/splash_icon.png b/android/app/src/main/res/drawable-hdpi/splash_icon.png
deleted file mode 100644
index 2f2efcf..0000000
Binary files a/android/app/src/main/res/drawable-hdpi/splash_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-mdpi/splash_icon.png b/android/app/src/main/res/drawable-mdpi/splash_icon.png
deleted file mode 100644
index 10f64d4..0000000
Binary files a/android/app/src/main/res/drawable-mdpi/splash_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splash_icon.png b/android/app/src/main/res/drawable-xhdpi/splash_icon.png
deleted file mode 100644
index f798d58..0000000
Binary files a/android/app/src/main/res/drawable-xhdpi/splash_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splash_icon.png b/android/app/src/main/res/drawable-xxhdpi/splash_icon.png
deleted file mode 100644
index caf0555..0000000
Binary files a/android/app/src/main/res/drawable-xxhdpi/splash_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash_icon.png b/android/app/src/main/res/drawable-xxxhdpi/splash_icon.png
deleted file mode 100644
index 0e78a9a..0000000
Binary files a/android/app/src/main/res/drawable-xxxhdpi/splash_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index c6431cb..7ae7661 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -3,5 +3,4 @@
#1F1B16
- #FFF1E5
diff --git a/lib/core/widgets/library_drama_tile.dart b/lib/core/widgets/library_drama_tile.dart
deleted file mode 100644
index 2c715ff..0000000
--- a/lib/core/widgets/library_drama_tile.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-// Author: fruit.com.cn
-
-import 'package:flutter/material.dart';
-
-import '../data/models/drama.dart';
-import '../theme/app_colors.dart';
-import '../theme/app_dimens.dart';
-import 'remote_image.dart';
-
-/// 收藏等账号列表复用的剧集卡片,视觉与"我的"页列表保持一致。
-class LibraryDramaTile extends StatelessWidget {
- const LibraryDramaTile({
- super.key,
- required this.drama,
- required this.onTap,
- this.trailing,
- });
-
- final Drama drama;
- final VoidCallback? onTap;
- final Widget? trailing;
-
- @override
- Widget build(BuildContext context) {
- return Material(
- color: AppColors.card,
- borderRadius: BorderRadius.circular(AppDimens.innerRadius),
- clipBehavior: Clip.antiAlias,
- child: InkWell(
- onTap: onTap,
- child: Padding(
- padding: const EdgeInsets.all(AppDimens.innerPadding),
- child: Row(
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(AppDimens.innerRadius),
- child: RemoteImage(drama.posterUrl, width: 74, height: 103),
- ),
- const SizedBox(width: 11),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- drama.title,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- color: Colors.white,
- fontSize: AppDimens.listTitle,
- fontWeight: FontWeight.w700,
- ),
- ),
- const SizedBox(height: 7),
- Text(
- drama.genre.isEmpty ? drama.category : drama.genre,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- color: AppColors.muted,
- fontSize: AppDimens.caption,
- ),
- ),
- ],
- ),
- ),
- ?trailing,
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/auth/auth_binding.dart b/lib/pages/auth/auth_binding.dart
deleted file mode 100644
index dbc17f7..0000000
--- a/lib/pages/auth/auth_binding.dart
+++ /dev/null
@@ -1,6 +0,0 @@
-import 'auth_logic.dart';
-
-/// 与其它页保持一致的三层写法:binding 负责装配,logic 放状态与逻辑,view 只渲染。
-class AuthBinding {
- AuthLogic create({bool signUpMode = false}) => AuthLogic(signUpMode: signUpMode);
-}
diff --git a/lib/pages/discover/category_dramas_page.dart b/lib/pages/discover/category_dramas_page.dart
deleted file mode 100644
index 9b35069..0000000
--- a/lib/pages/discover/category_dramas_page.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-// Author: fruit.com.cn
-
-import 'package:flutter/material.dart';
-
-import '../../core/data/models/drama.dart';
-import '../../core/theme/app_theme.dart';
-import '../../core/widgets/empty_state.dart';
-import '../../core/widgets/plain_page_scaffold.dart';
-import '../../l10n/app_localizations.dart';
-import '../drama_detail/drama_detail_view.dart';
-import 'widgets/drama_grid.dart';
-
-/// 从剧场分类九宫格进入的分类剧目列表。
-class CategoryDramasPage extends StatelessWidget {
- const CategoryDramasPage({
- super.key,
- required this.title,
- required this.dramas,
- this.onOpen,
- });
-
- final String title;
- final List dramas;
- final ValueChanged? onOpen;
-
- @override
- Widget build(BuildContext context) {
- final l10n = AppLocalizations.of(context)!;
- return Theme(
- data: AppTheme.light(),
- child: PlainPageScaffold(
- pageKey: const Key('category-dramas-page'),
- backKey: const Key('category-dramas-back'),
- title: title,
- child: dramas.isEmpty
- ? EmptyState(
- key: const Key('category-dramas-empty'),
- icon: Icons.movie_filter_outlined,
- title: l10n.noCategoryDramas,
- subtitle: l10n.emptyHint,
- )
- : SingleChildScrollView(
- padding: const EdgeInsets.only(top: 8, bottom: 28),
- child: DramaGrid(
- l10n: l10n,
- dramas: dramas,
- onOpen: (drama) => _open(context, drama),
- ),
- ),
- ),
- );
- }
-
- void _open(BuildContext context, Drama drama) {
- final callback = onOpen;
- if (callback != null) {
- callback(drama);
- return;
- }
- Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => DramaDetailPage(drama: drama)),
- );
- }
-}
diff --git a/lib/pages/discover/widgets/category_grid.dart b/lib/pages/discover/widgets/category_grid.dart
deleted file mode 100644
index 63ae42f..0000000
--- a/lib/pages/discover/widgets/category_grid.dart
+++ /dev/null
@@ -1,114 +0,0 @@
-// Author: fruit.com.cn
-
-import 'package:flutter/material.dart';
-
-import '../../../core/data/models/content_category.dart';
-import '../../../core/theme/app_colors.dart';
-
-const double _gridPadding = 14;
-const double _tileSpacing = 8;
-const double _tileAspectRatio = 1.1;
-const int _columns = 3;
-const List _icons = [
- Icons.favorite_rounded,
- Icons.bolt_rounded,
- Icons.theater_comedy_rounded,
- Icons.auto_awesome_rounded,
- Icons.psychology_alt_rounded,
- Icons.local_fire_department_rounded,
- Icons.family_restroom_rounded,
- Icons.work_history_rounded,
-];
-
-/// 剧场顶部分类九宫格。
-class CategoryGrid extends StatelessWidget {
- const CategoryGrid({
- super.key,
- required this.categories,
- required this.onTap,
- });
-
- final List categories;
- final ValueChanged onTap;
-
- @override
- Widget build(BuildContext context) {
- if (categories.isEmpty) return const SizedBox.shrink();
- return GridView.builder(
- key: const Key('discover-category-grid'),
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- padding: const EdgeInsets.symmetric(horizontal: _gridPadding),
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: _columns,
- mainAxisSpacing: _tileSpacing,
- crossAxisSpacing: _tileSpacing,
- childAspectRatio: _tileAspectRatio,
- ),
- itemCount: categories.length,
- itemBuilder: (context, index) {
- final category = categories[index];
- return _CategoryTile(
- category: category,
- icon: _icons[index % _icons.length],
- onTap: () => onTap(category),
- );
- },
- );
- }
-}
-
-class _CategoryTile extends StatelessWidget {
- const _CategoryTile({
- required this.category,
- required this.icon,
- required this.onTap,
- });
-
- final ContentCategory category;
- final IconData icon;
- final VoidCallback onTap;
-
- @override
- Widget build(BuildContext context) {
- return Material(
- key: Key('discover-category-${category.id}'),
- color: AppColors.card,
- borderRadius: BorderRadius.circular(12),
- clipBehavior: Clip.antiAlias,
- child: InkWell(
- onTap: onTap,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Container(
- width: 30,
- height: 30,
- decoration: BoxDecoration(
- color: AppColors.blueSoft.withValues(alpha: 0.16),
- shape: BoxShape.circle,
- ),
- child: Icon(icon, color: AppColors.blueSoft, size: 16),
- ),
- const SizedBox(height: 5),
- Text(
- category.name,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- textAlign: TextAlign.center,
- style: const TextStyle(
- color: AppColors.onSurface,
- fontSize: 9,
- fontWeight: FontWeight.w600,
- height: 1.1,
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/discover/widgets/ranking_section.dart b/lib/pages/discover/widgets/ranking_section.dart
deleted file mode 100644
index e3ade9d..0000000
--- a/lib/pages/discover/widgets/ranking_section.dart
+++ /dev/null
@@ -1,268 +0,0 @@
-// Author: fruit.com.cn
-
-import 'package:flutter/material.dart';
-
-import '../../../core/data/models/drama.dart';
-import '../../../core/data/models/home_feed.dart';
-import '../../../core/theme/app_colors.dart';
-import '../../../core/widgets/empty_state.dart';
-import '../../../core/widgets/remote_image.dart';
-import '../../../l10n/app_localizations.dart';
-import '../ranking_logic.dart';
-
-const int _topLimit = 5;
-const double _rowHeight = 68;
-const double _posterWidth = 40;
-const double _posterHeight = 54;
-const double _badgeSize = 24;
-
-/// 发现页内嵌榜单区块:热播/新剧切换 + 前 5 名。
-class RankingSection extends StatefulWidget {
- const RankingSection({
- super.key,
- required this.sections,
- required this.fallback,
- required this.onOpen,
- });
-
- final List sections;
- final List fallback;
- final ValueChanged onOpen;
-
- @override
- State createState() => _RankingSectionState();
-}
-
-class _RankingSectionState extends State {
- late final RankingLogic logic = RankingLogic(
- sections: widget.sections,
- fallback: widget.fallback,
- );
-
- @override
- void didUpdateWidget(covariant RankingSection oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (!identical(oldWidget.sections, widget.sections) ||
- !identical(oldWidget.fallback, widget.fallback)) {
- logic.updateSources(sections: widget.sections, fallback: widget.fallback);
- }
- }
-
- @override
- void dispose() {
- logic.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) => ListenableBuilder(
- listenable: logic,
- builder: (context, _) => _content(context),
- );
-
- Widget _content(BuildContext context) {
- final l10n = AppLocalizations.of(context)!;
- final entries = logic.entries.take(_topLimit).toList();
- return Container(
- key: const Key('discover-ranking-section'),
- margin: const EdgeInsets.fromLTRB(14, 16, 14, 8),
- padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
- decoration: BoxDecoration(
- color: AppColors.card,
- borderRadius: BorderRadius.circular(14),
- border: Border.all(color: AppColors.border),
- ),
- child: Column(
- children: [
- _header(l10n),
- const SizedBox(height: 8),
- if (entries.isEmpty)
- EmptyState(
- key: const Key('ranking-empty'),
- icon: Icons.leaderboard_outlined,
- title: l10n.rankingEmptyTitle,
- subtitle: l10n.rankingEmptyBody,
- compact: true,
- )
- else
- for (final entry in entries) _row(entry, l10n),
- ],
- ),
- );
- }
-
- Widget _header(AppLocalizations l10n) {
- return Row(
- children: [
- const Icon(
- Icons.local_fire_department_rounded,
- color: AppColors.blue,
- size: 18,
- ),
- const SizedBox(width: 6),
- Expanded(
- child: Text(
- l10n.rankingTitle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- color: AppColors.onSurface,
- fontSize: 13,
- fontWeight: FontWeight.w800,
- ),
- ),
- ),
- _boardChip(
- key: const Key('ranking-tab-hot'),
- label: l10n.rankingHot,
- board: RankingBoard.hot,
- ),
- const SizedBox(width: 6),
- _boardChip(
- key: const Key('ranking-tab-new'),
- label: l10n.rankingNew,
- board: RankingBoard.newReleases,
- ),
- ],
- );
- }
-
- Widget _boardChip({
- required Key key,
- required String label,
- required RankingBoard board,
- }) {
- final selected = logic.board == board;
- return Material(
- key: key,
- color: selected ? AppColors.blue : AppColors.card,
- borderRadius: BorderRadius.circular(999),
- clipBehavior: Clip.antiAlias,
- child: InkWell(
- onTap: () => logic.select(board),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5),
- child: Text(
- label,
- style: TextStyle(
- color: selected ? AppColors.onPrimary : AppColors.onSurface,
- fontSize: 9,
- fontWeight: FontWeight.w700,
- ),
- ),
- ),
- ),
- );
- }
-
- Widget _row(RankingEntry entry, AppLocalizations l10n) {
- return _RankRow(
- entry: entry,
- subtitle: _subtitle(entry.drama, l10n),
- onTap: () => widget.onOpen(entry.drama),
- );
- }
-
- String _subtitle(Drama drama, AppLocalizations l10n) {
- final genre = drama.genre.isNotEmpty ? drama.genre : drama.category;
- final count = l10n.episodeCount(drama.episodeCount);
- return genre.isEmpty ? count : '$genre · $count';
- }
-}
-
-class _RankRow extends StatelessWidget {
- const _RankRow({
- required this.entry,
- required this.subtitle,
- required this.onTap,
- });
-
- final RankingEntry entry;
- final String subtitle;
- final VoidCallback onTap;
-
- @override
- Widget build(BuildContext context) {
- final drama = entry.drama;
- return Material(
- key: Key('ranking-entry-${drama.id}'),
- color: Colors.transparent,
- child: InkWell(
- onTap: onTap,
- borderRadius: BorderRadius.circular(10),
- child: SizedBox(height: _rowHeight, child: _content(drama)),
- ),
- );
- }
-
- Widget _content(Drama drama) {
- return Row(
- children: [
- _rankBadge(entry.rank),
- const SizedBox(width: 8),
- ClipRRect(
- borderRadius: BorderRadius.circular(7),
- child: RemoteImage(
- drama.posterUrl,
- width: _posterWidth,
- height: _posterHeight,
- ),
- ),
- const SizedBox(width: 10),
- Expanded(child: _text(drama)),
- const Icon(
- Icons.chevron_right_rounded,
- color: AppColors.subtle,
- size: 18,
- ),
- ],
- );
- }
-
- Widget _text(Drama drama) {
- return Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- drama.title,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- color: AppColors.onSurface,
- fontSize: 11,
- fontWeight: FontWeight.w700,
- ),
- ),
- const SizedBox(height: 3),
- Text(
- subtitle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(color: AppColors.subtle, fontSize: 9),
- ),
- ],
- );
- }
-
- Widget _rankBadge(int rank) {
- final top = rank <= 3;
- return Container(
- width: _badgeSize,
- height: _badgeSize,
- alignment: Alignment.center,
- decoration: BoxDecoration(
- color: top ? AppColors.gold.withValues(alpha: 0.16) : AppColors.raised,
- borderRadius: BorderRadius.circular(7),
- ),
- child: Text(
- '$rank',
- style: TextStyle(
- color: top ? AppColors.gold : AppColors.muted,
- fontSize: 11,
- fontWeight: FontWeight.w900,
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/home/home_browse_logic.dart b/lib/pages/home/home_browse_logic.dart
deleted file mode 100644
index 907c614..0000000
--- a/lib/pages/home/home_browse_logic.dart
+++ /dev/null
@@ -1,70 +0,0 @@
-// Author: fruit.com.cn
-
-import 'dart:async';
-
-import 'package:flutter/foundation.dart';
-
-import '../../core/data/models/watch_progress.dart';
-import '../../core/data/repositories/watch_history_repository.dart';
-
-/// 首页频道,只依赖目录中已有数据排序或过滤。
-enum HomeChannel { hot, free, newReleases, ranking }
-
-/// 浏览型首页的本地 UI 状态。
-class HomeBrowseLogic extends ChangeNotifier {
- HomeBrowseLogic({required this.history}) {
- unawaited(refreshRecent());
- }
-
- static const int recentLimit = 20;
-
- final WatchHistoryRepository history;
- HomeChannel channel = HomeChannel.hot;
- List recent = const [];
- bool loadingRecent = false;
- bool _disposed = false;
-
- /// 第一条未完成且存在真实播放位置的记录。
- WatchProgress? get primaryRecord {
- for (final record in recent) {
- if (!record.finished && record.hasPosition) return record;
- }
- return null;
- }
-
- /// 切换频道,不触发额外网络请求。
- void selectChannel(HomeChannel value) {
- if (channel == value) return;
- channel = value;
- _notify();
- }
-
- /// 重新加载续看数据;未登录时返回空列表。
- Future refreshRecent() async {
- loadingRecent = true;
- _notify();
- try {
- recent = await history.recent(limit: recentLimit);
- } catch (_) {
- recent = const [];
- } finally {
- loadingRecent = false;
- _notify();
- }
- }
-
- /// 首页重新可见时刷新续看数据。
- void syncPage({required bool active, required bool wasActive}) {
- if (active && !wasActive) unawaited(refreshRecent());
- }
-
- void _notify() {
- if (!_disposed) notifyListeners();
- }
-
- @override
- void dispose() {
- _disposed = true;
- super.dispose();
- }
-}
diff --git a/lib/pages/home/home_browse_view.dart b/lib/pages/home/home_browse_view.dart
deleted file mode 100644
index 7a13bf0..0000000
--- a/lib/pages/home/home_browse_view.dart
+++ /dev/null
@@ -1,451 +0,0 @@
-// Author: fruit.com.cn
-
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-import '../../core/data/models/drama.dart';
-import '../../core/data/models/home_feed.dart';
-import '../../core/data/models/watch_progress.dart';
-import '../../core/data/repositories/watch_history_repository.dart';
-import '../../core/data/services/catalog_controller.dart';
-import '../../core/theme/app_colors.dart';
-import '../../core/theme/app_dimens.dart';
-import '../../core/widgets/app_pull_to_refresh.dart';
-import '../../core/widgets/empty_state.dart';
-import '../../l10n/app_localizations.dart';
-import '../discover/widgets/feed_banner_carousel.dart';
-import '../mine/widgets/continue_watching_card.dart';
-import 'home_browse_logic.dart';
-import 'widgets/home_browse_header.dart';
-import 'widgets/home_drama_grid.dart';
-
-const double _channelRowHeight = 44;
-const double _channelGap = 20;
-const double _channelUnderlineWidth = 18;
-const double _channelUnderlineHeight = 2;
-const double _channelUnderlineGap = 4;
-const double _channelFontSize = 15;
-const List _channelTextShadows = [
- Shadow(color: Colors.black54, blurRadius: 5, offset: Offset(0, 1)),
-];
-
-/// 浏览型首页:频道、Hero、继续观看与剧目目录。
-class HomeBrowseView extends StatefulWidget {
- const HomeBrowseView({
- super.key,
- required this.active,
- required this.onSearch,
- required this.onProfile,
- required this.onTheater,
- required this.onHistory,
- required this.onOpenDrama,
- required this.onOpenContinue,
- });
-
- final bool active;
- final VoidCallback onSearch;
- final VoidCallback onProfile;
- final VoidCallback onTheater;
- final VoidCallback onHistory;
- final Future Function(Drama drama) onOpenDrama;
- final Future Function(Drama drama, WatchProgress record) onOpenContinue;
-
- @override
- State createState() => _HomeBrowseViewState();
-}
-
-class _HomeBrowseViewState extends State {
- late final HomeBrowseLogic logic;
- final ScrollController _scroll = ScrollController();
- bool _channelsPinned = false;
-
- @override
- void initState() {
- super.initState();
- logic = HomeBrowseLogic(history: context.read());
- _scroll.addListener(_onScroll);
- }
-
- @override
- void didUpdateWidget(covariant HomeBrowseView oldWidget) {
- super.didUpdateWidget(oldWidget);
- logic.syncPage(active: widget.active, wasActive: oldWidget.active);
- }
-
- @override
- void dispose() {
- _scroll.removeListener(_onScroll);
- _scroll.dispose();
- logic.dispose();
- super.dispose();
- }
-
- /// 滚过 hero 后频道行切成吸顶态,未过阈值时仍叠在轮播图上。
- void _onScroll() {
- if (!_scroll.hasClients) return;
- final pinned =
- _scroll.offset >= AppDimens.heroBannerHeight - _channelRowHeight;
- if (pinned != _channelsPinned) {
- setState(() => _channelsPinned = pinned);
- }
- }
-
- /// 频道行只横向滚动,纵向拖动转交给页面列表,避免挡住上滑。
- void _scrollPageBy(DragUpdateDetails details) {
- if (!_scroll.hasClients) return;
- final position = _scroll.position;
- _scroll.jumpTo(
- (_scroll.offset - details.delta.dy)
- .clamp(position.minScrollExtent, position.maxScrollExtent)
- .toDouble(),
- );
- }
-
- @override
- Widget build(BuildContext context) {
- return ListenableBuilder(
- listenable: logic,
- builder: (context, _) => _page(context),
- );
- }
-
- Widget _page(BuildContext context) {
- final l10n = AppLocalizations.of(context)!;
- final catalog = context.watch();
- return SafeArea(
- bottom: false,
- child: Column(
- children: [
- HomeBrowseHeader(
- onSearch: widget.onSearch,
- onProfile: widget.onProfile,
- ),
- Expanded(child: _body(l10n, catalog)),
- ],
- ),
- );
- }
-
- Widget _body(AppLocalizations l10n, CatalogController catalog) {
- final source = catalog.dramas;
- final banners = catalog.banners.isEmpty
- ? _fallbackBanners(source)
- : catalog.banners;
- final hasBanner = banners.isNotEmpty;
- final pinned = !hasBanner || _channelsPinned;
- return Stack(
- children: [
- Positioned.fill(
- child: AppPullToRefresh(
- key: const Key('home-refresh'),
- onRefresh: () async {
- await Future.wait([catalog.load(), logic.refreshRecent()]);
- },
- child: ListView(
- key: const Key('home-browse-page'),
- controller: _scroll,
- physics: AppPullToRefresh.physics,
- padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
- children: _sections(l10n, catalog),
- ),
- ),
- ),
- Positioned(
- top: 0,
- left: 0,
- right: 0,
- child: _channelBar(
- l10n,
- pinned: pinned,
- overImage: hasBanner && !pinned,
- ),
- ),
- ],
- );
- }
-
- Widget _channelBar(
- AppLocalizations l10n, {
- required bool pinned,
- required bool overImage,
- }) {
- final content = GestureDetector(
- behavior: HitTestBehavior.opaque,
- onVerticalDragUpdate: _scrollPageBy,
- child: _channels(l10n, overImage: overImage),
- );
- if (!pinned) return content;
- return DecoratedBox(
- key: const Key('home-channels-pinned'),
- decoration: const BoxDecoration(
- color: AppColors.ink,
- border: Border(bottom: BorderSide(color: AppColors.border)),
- ),
- child: content,
- );
- }
-
- List _sections(AppLocalizations l10n, CatalogController catalog) {
- final source = catalog.dramas;
- final dramas = _channelDramas(source, logic.channel);
- final continueItem = _continueItem(logic.recent, catalog.dramas);
- final banners = catalog.banners.isEmpty
- ? _fallbackBanners(source)
- : catalog.banners;
- return [
- if (banners.isNotEmpty) _hero(banners, source),
- if (continueItem != null) ...[
- _continueHeader(l10n),
- ContinueWatchingCard(
- cardKey: const Key('home-continue-watching'),
- playButtonKey: Key('home-continue-play-${continueItem.drama.id}'),
- progressKey: Key('home-continue-progress-${continueItem.drama.id}'),
- showRemaining: true,
- drama: continueItem.drama,
- record: continueItem.record,
- onResume: () => unawaited(
- widget.onOpenContinue(continueItem.drama, continueItem.record),
- ),
- ),
- ],
- _channelHeader(l10n),
- if (dramas.isEmpty)
- _empty()
- else
- HomeDramaGrid(
- l10n: l10n,
- dramas: dramas,
- onOpen: (drama) => unawaited(widget.onOpenDrama(drama)),
- ),
- ];
- }
-
- List _fallbackBanners(List dramas) {
- if (dramas.isEmpty) return const [];
- final drama = dramas.first;
- return [
- FeedBanner(
- id: 'home-fallback-${drama.id}',
- imageUrl: drama.posterUrl,
- linkType: 'DRAMA',
- linkValue: drama.id,
- ),
- ];
- }
-
- Widget _continueHeader(AppLocalizations l10n) => _sectionHeader(
- l10n.continueWatching,
- actionKey: const Key('home-continue-all'),
- actionLabel: l10n.seeAll,
- onAction: widget.onHistory,
- );
-
- Widget _channelHeader(AppLocalizations l10n) => _sectionHeader(
- _channelTitle(l10n),
- actionKey: const Key('home-popular-more'),
- actionLabel: l10n.seeAll,
- onAction: widget.onTheater,
- );
-
- Widget _empty() => Padding(
- padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceXl),
- child: EmptyState(
- key: const Key('home-browse-empty'),
- icon: Icons.movie_filter_outlined,
- title: AppLocalizations.of(context)!.noStories,
- subtitle: AppLocalizations.of(context)!.emptyHint,
- compact: true,
- ),
- );
-
- Widget _channels(AppLocalizations l10n, {bool overImage = false}) {
- final entries = [
- (HomeChannel.hot, l10n.hot),
- (HomeChannel.free, l10n.free),
- (HomeChannel.newReleases, l10n.newDramas),
- (HomeChannel.ranking, l10n.rankingTab),
- ];
- return SizedBox(
- key: const Key('home-channels'),
- height: _channelRowHeight,
- child: ListView.separated(
- scrollDirection: Axis.horizontal,
- padding: const EdgeInsets.symmetric(
- horizontal: AppDimens.pageHorizontal,
- ),
- itemCount: entries.length,
- separatorBuilder: (_, _) => const SizedBox(width: _channelGap),
- itemBuilder: (context, index) {
- final entry = entries[index];
- final selected = logic.channel == entry.$1;
- return _channelItem(
- entry.$1,
- entry.$2,
- selected: selected,
- overImage: overImage,
- );
- },
- ),
- );
- }
-
- Widget _channelItem(
- HomeChannel channel,
- String label, {
- required bool selected,
- required bool overImage,
- }) {
- final color = selected
- ? AppColors.blue
- : (overImage ? Colors.white70 : AppColors.muted);
- final underlineColor = selected ? AppColors.blue : Colors.transparent;
- return Semantics(
- button: true,
- selected: selected,
- child: GestureDetector(
- key: Key('home-channel-${channel.name}'),
- behavior: HitTestBehavior.opaque,
- onTap: () => logic.selectChannel(channel),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(
- label,
- maxLines: 1,
- style: TextStyle(
- color: color,
- fontSize: _channelFontSize,
- fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
- shadows: overImage ? _channelTextShadows : null,
- ),
- ),
- const SizedBox(height: _channelUnderlineGap),
- Container(
- key: Key('home-channel-underline-${channel.name}'),
- width: _channelUnderlineWidth,
- height: _channelUnderlineHeight,
- decoration: BoxDecoration(
- color: underlineColor,
- borderRadius: BorderRadius.circular(
- _channelUnderlineHeight / 2,
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _hero(List banners, List dramas) {
- return FeedBannerCarousel(
- key: const Key('home-hero-banner'),
- banners: banners,
- active: widget.active,
- flush: true,
- dramaFor: (banner) => _dramaForBanner(banner, dramas),
- onOpen: (banner) {
- final drama = _dramaForBanner(banner, dramas);
- if (drama != null) unawaited(widget.onOpenDrama(drama));
- },
- );
- }
-
- Widget _sectionHeader(
- String title, {
- required Key actionKey,
- required String actionLabel,
- required VoidCallback onAction,
- }) {
- return Padding(
- padding: const EdgeInsets.fromLTRB(
- AppDimens.pageHorizontal,
- AppDimens.spaceLg,
- AppDimens.spaceSm,
- AppDimens.spaceSm,
- ),
- child: Row(
- children: [
- Expanded(
- child: Text(
- title,
- style: const TextStyle(
- fontFamily: 'Outfit',
- color: AppColors.onSurface,
- fontSize: AppDimens.sectionTitle,
- fontWeight: FontWeight.w800,
- ),
- ),
- ),
- TextButton(
- key: actionKey,
- onPressed: onAction,
- style: TextButton.styleFrom(
- foregroundColor: AppColors.accent,
- minimumSize: const Size(AppDimens.tapTarget, AppDimens.tapTarget),
- ),
- child: Text(actionLabel),
- ),
- ],
- ),
- );
- }
-
- String _channelTitle(AppLocalizations l10n) {
- return switch (logic.channel) {
- HomeChannel.hot => l10n.trendingMicroDramas,
- HomeChannel.free => l10n.free,
- HomeChannel.newReleases => l10n.newDramas,
- HomeChannel.ranking => l10n.rankingTitle,
- };
- }
-
- List _channelDramas(List source, HomeChannel channel) {
- final dramas = List.of(source);
- switch (channel) {
- case HomeChannel.hot:
- dramas.sort((a, b) {
- final hot = (b.isHot ? 1 : 0).compareTo(a.isHot ? 1 : 0);
- return hot != 0 ? hot : b.viewCount.compareTo(a.viewCount);
- });
- case HomeChannel.free:
- return dramas.where(_isFree).toList();
- case HomeChannel.newReleases:
- return dramas;
- case HomeChannel.ranking:
- dramas.sort((a, b) => b.viewCount.compareTo(a.viewCount));
- }
- return dramas;
- }
-
- bool _isFree(Drama drama) {
- return drama.freeEpisodes > 0 ||
- drama.episodes.any((episode) => episode.isFree);
- }
-
- Drama? _dramaForBanner(FeedBanner banner, List dramas) {
- final id = banner.dramaId;
- if (id == null) return null;
- for (final drama in dramas) {
- if (drama.id == id) return drama;
- }
- return null;
- }
-
- ({Drama drama, WatchProgress record})? _continueItem(
- List records,
- List dramas,
- ) {
- for (final record in records) {
- if (record.finished || !record.hasPosition) continue;
- for (final drama in dramas) {
- if (drama.id == record.dramaId) {
- return (drama: drama, record: record);
- }
- }
- }
- return null;
- }
-}
diff --git a/lib/pages/home/widgets/home_browse_header.dart b/lib/pages/home/widgets/home_browse_header.dart
deleted file mode 100644
index e6554a4..0000000
--- a/lib/pages/home/widgets/home_browse_header.dart
+++ /dev/null
@@ -1,137 +0,0 @@
-// Author: fruit.com.cn
-
-import 'package:flutter/material.dart';
-
-import '../../../core/theme/app_colors.dart';
-import '../../../core/theme/app_dimens.dart';
-import '../../../l10n/app_localizations.dart';
-import '../../notices/notices_page.dart';
-
-const double _searchHeight = 36;
-
-/// 首页固定顶栏:搜索、通知与头像入口。
-class HomeBrowseHeader extends StatelessWidget {
- const HomeBrowseHeader({
- super.key,
- required this.onSearch,
- required this.onProfile,
- });
-
- final VoidCallback onSearch;
- final VoidCallback onProfile;
-
- @override
- Widget build(BuildContext context) {
- final l10n = AppLocalizations.of(context)!;
- return SizedBox(
- height: AppDimens.contentHeaderHeight,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: AppDimens.pageHorizontal,
- ),
- child: Row(
- children: [
- Expanded(child: _search(l10n, onSearch)),
- const SizedBox(width: AppDimens.spaceSm),
- _notifications(context, l10n),
- const SizedBox(width: AppDimens.spaceSm),
- _profile(l10n),
- ],
- ),
- ),
- );
- }
-
- Widget _search(AppLocalizations l10n, VoidCallback onTap) {
- return Semantics(
- button: true,
- label: l10n.search,
- child: InkWell(
- key: const Key('home-search'),
- onTap: onTap,
- borderRadius: BorderRadius.circular(AppDimens.pillRadius),
- child: Container(
- height: _searchHeight,
- padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceMd),
- decoration: BoxDecoration(
- color: AppColors.input,
- borderRadius: BorderRadius.circular(AppDimens.pillRadius),
- ),
- child: Row(
- children: [
- const Icon(
- Icons.search_rounded,
- color: AppColors.subtle,
- size: 17,
- ),
- const SizedBox(width: AppDimens.spaceSm),
- Expanded(
- child: Text(
- l10n.searchDiscover,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- color: AppColors.subtle,
- fontSize: AppDimens.caption,
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
-
- Widget _notifications(BuildContext context, AppLocalizations l10n) {
- return Semantics(
- button: true,
- label: l10n.notices,
- child: InkWell(
- key: const Key('home-notifications'),
- customBorder: const CircleBorder(),
- onTap: () => Navigator.of(
- context,
- ).push(MaterialPageRoute(builder: (_) => const NoticesPage())),
- child: Container(
- width: AppDimens.tapTarget,
- height: AppDimens.tapTarget,
- decoration: const BoxDecoration(
- color: AppColors.card,
- shape: BoxShape.circle,
- ),
- child: const Icon(
- Icons.notifications_none_rounded,
- color: AppColors.onSurface,
- size: AppDimens.accountIcon,
- ),
- ),
- ),
- );
- }
-
- Widget _profile(AppLocalizations l10n) {
- return Semantics(
- button: true,
- label: l10n.myTab,
- child: InkWell(
- key: const Key('home-profile'),
- customBorder: const CircleBorder(),
- onTap: onProfile,
- child: Container(
- width: AppDimens.tapTarget,
- height: AppDimens.tapTarget,
- decoration: const BoxDecoration(
- color: AppColors.card,
- shape: BoxShape.circle,
- ),
- child: const Icon(
- Icons.person_rounded,
- color: AppColors.onSurface,
- size: AppDimens.accountIcon,
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/home/widgets/home_menu_sheet.dart b/lib/pages/home/widgets/home_menu_sheet.dart
deleted file mode 100644
index 0a255b3..0000000
--- a/lib/pages/home/widgets/home_menu_sheet.dart
+++ /dev/null
@@ -1,228 +0,0 @@
-// Author: fruit.com.cn
-
-import 'package:flutter/material.dart';
-
-import '../../../core/data/models/drama.dart';
-import '../../../core/formatting/localized_content.dart';
-import '../../../core/theme/app_colors.dart';
-import '../../../core/widgets/remote_image.dart';
-import '../../../l10n/app_localizations.dart';
-
-/// 播放页快捷菜单:当前剧、发现、追剧与历史。
-class HomeMenuSheet extends StatelessWidget {
- const HomeMenuSheet({
- super.key,
- required this.drama,
- required this.onOpenSeries,
- required this.onDiscover,
- required this.onHistory,
- required this.onWatchlist,
- });
-
- final Drama drama;
- final VoidCallback onOpenSeries;
- final VoidCallback onDiscover;
- final VoidCallback onHistory;
- final VoidCallback onWatchlist;
-
- @override
- Widget build(BuildContext context) {
- final l10n = AppLocalizations.of(context)!;
- void closeThen(VoidCallback action) {
- Navigator.pop(context);
- action();
- }
-
- return SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.fromLTRB(16, 4, 16, 20),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- _DramaSummary(
- drama: drama,
- l10n: l10n,
- onOpenSeries: () => closeThen(onOpenSeries),
- ),
- const SizedBox(height: 14),
- _quickAccess(l10n, closeThen),
- ],
- ),
- ),
- );
- }
-
- Widget _quickAccess(
- AppLocalizations l10n,
- void Function(VoidCallback) closeThen,
- ) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- l10n.quickAccess,
- style: const TextStyle(
- color: AppColors.onSurface,
- fontSize: 13,
- fontWeight: FontWeight.w800,
- ),
- ),
- const SizedBox(height: 8),
- Row(
- children: [
- _item(
- key: const Key('drawer-discover'),
- icon: Icons.explore_outlined,
- label: l10n.discoverTab,
- onTap: () => closeThen(onDiscover),
- ),
- _item(
- key: const Key('drawer-watchlist'),
- icon: Icons.bookmark_border_rounded,
- label: l10n.watchlist,
- onTap: () => closeThen(onWatchlist),
- ),
- _item(
- key: const Key('drawer-history'),
- icon: Icons.history_rounded,
- label: l10n.history,
- onTap: () => closeThen(onHistory),
- ),
- ],
- ),
- ],
- );
- }
-
- Widget _item({
- required Key key,
- required IconData icon,
- required String label,
- required VoidCallback onTap,
- }) {
- return Expanded(
- child: Padding(
- padding: const EdgeInsets.only(right: 8),
- child: _MenuTile(key: key, icon: icon, label: label, onTap: onTap),
- ),
- );
- }
-}
-
-class _DramaSummary extends StatelessWidget {
- const _DramaSummary({
- required this.drama,
- required this.l10n,
- required this.onOpenSeries,
- });
-
- final Drama drama;
- final AppLocalizations l10n;
- final VoidCallback onOpenSeries;
-
- @override
- Widget build(BuildContext context) {
- return Container(
- padding: const EdgeInsets.all(10),
- decoration: BoxDecoration(
- color: AppColors.cardAlt,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Row(
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(10),
- child: RemoteImage(drama.posterUrl, width: 64, height: 86),
- ),
- const SizedBox(width: 10),
- Expanded(child: _details()),
- ],
- ),
- );
- }
-
- Widget _details() {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- localizedDramaTitle(l10n, drama),
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- color: AppColors.onSurface,
- fontSize: 15,
- fontWeight: FontWeight.w800,
- ),
- ),
- const SizedBox(height: 4),
- Text(
- l10n.episodeCount(drama.episodeCount),
- style: const TextStyle(color: AppColors.muted, fontSize: 9),
- ),
- const SizedBox(height: 8),
- _button(),
- ],
- );
- }
-
- Widget _button() {
- return SizedBox(
- height: 34,
- child: FilledButton.icon(
- key: const Key('drawer-open-series'),
- onPressed: onOpenSeries,
- style: FilledButton.styleFrom(
- backgroundColor: AppColors.blue,
- foregroundColor: AppColors.onPrimary,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(9)),
- textStyle: const TextStyle(fontSize: 11),
- ),
- icon: const Icon(Icons.play_arrow_rounded, size: 15),
- label: Text(l10n.exploreSeries),
- ),
- );
- }
-}
-
-class _MenuTile extends StatelessWidget {
- const _MenuTile({
- super.key,
- required this.icon,
- required this.label,
- required this.onTap,
- });
-
- final IconData icon;
- final String label;
- final VoidCallback onTap;
-
- @override
- Widget build(BuildContext context) {
- return Material(
- color: AppColors.quick,
- borderRadius: BorderRadius.circular(12),
- clipBehavior: Clip.antiAlias,
- child: InkWell(
- onTap: onTap,
- child: SizedBox(
- height: 58,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(icon, color: AppColors.secondary, size: 16),
- const SizedBox(height: 4),
- Text(
- label,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(color: AppColors.muted, fontSize: 9),
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/pages/mine/widgets/continue_watching_card.dart b/lib/pages/mine/widgets/continue_watching_card.dart
deleted file mode 100644
index 6e17981..0000000
--- a/lib/pages/mine/widgets/continue_watching_card.dart
+++ /dev/null
@@ -1,131 +0,0 @@
-// Author: fruit.com.cn
-
-import 'package:flutter/material.dart';
-
-import '../../../core/data/models/drama.dart';
-import '../../../core/data/models/watch_progress.dart';
-import '../../../core/theme/app_colors.dart';
-import '../../../core/widgets/remote_image.dart';
-import '../../../l10n/app_localizations.dart';
-
-/// 「我的」页唯一保留的续看入口。
-class ContinueWatchingCard extends StatelessWidget {
- const ContinueWatchingCard({
- super.key,
- required this.drama,
- required this.record,
- required this.onResume,
- this.cardKey = const Key('mine-continue-watching'),
- this.playButtonKey = const Key('mine-continue-play'),
- this.progressKey,
- this.showRemaining = false,
- });
-
- final Drama drama;
- final WatchProgress record;
- final VoidCallback onResume;
- final Key cardKey;
- final Key playButtonKey;
- final Key? progressKey;
- final bool showRemaining;
-
- @override
- Widget build(BuildContext context) {
- final l10n = AppLocalizations.of(context)!;
- return Container(
- key: cardKey,
- margin: const EdgeInsets.symmetric(horizontal: 14),
- padding: const EdgeInsets.all(9),
- decoration: BoxDecoration(
- color: AppColors.card,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Row(
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(7),
- child: RemoteImage(drama.posterUrl, width: 50, height: 64),
- ),
- const SizedBox(width: 9),
- Expanded(child: _info(l10n)),
- const SizedBox(width: 8),
- SizedBox(
- width: 60,
- height: 28,
- child: FilledButton(
- key: playButtonKey,
- onPressed: onResume,
- style: FilledButton.styleFrom(
- backgroundColor: AppColors.blue,
- foregroundColor: AppColors.onPrimary,
- padding: EdgeInsets.zero,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(9),
- ),
- textStyle: const TextStyle(fontSize: 10),
- ),
- child: Text(l10n.resume),
- ),
- ),
- ],
- ),
- );
- }
-
- Widget _info(AppLocalizations l10n) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- l10n.continueWatching,
- style: const TextStyle(
- color: AppColors.accent,
- fontSize: 9,
- fontWeight: FontWeight.w800,
- ),
- ),
- const SizedBox(height: 2),
- Text(
- drama.title,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- color: AppColors.onSurface,
- fontSize: 12,
- fontWeight: FontWeight.w800,
- ),
- ),
- const SizedBox(height: 2),
- Text(
- _progressText(l10n),
- key: progressKey,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(color: AppColors.muted, fontSize: 9),
- ),
- ],
- );
- }
-
- String _progressText(AppLocalizations l10n) {
- final episode = drama.episodes.where((item) => item.id == record.episodeId);
- if (showRemaining) {
- final number = episode.isEmpty ? 1 : episode.first.number;
- final remaining = record.duration - record.position;
- final minutes = remaining.inSeconds <= 0
- ? 0
- : (remaining.inSeconds / Duration.secondsPerMinute).ceil();
- return l10n.episodeRemaining(number, minutes);
- }
- final label = episode.isEmpty
- ? l10n.episodeCount(drama.episodeCount)
- : l10n.episodeOfTotal(episode.first.number, drama.episodeCount);
- return '$label · ${_clock(record.position)} / ${_clock(record.duration)}';
- }
-}
-
-String _clock(Duration value) {
- final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0');
- final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0');
- return '$minutes:$seconds';
-}