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'; -}