feat(flutter): 应用浅色暖橙视觉主题

This commit is contained in:
Zhou Weidong committed 2026-10-20 10:15:03 +08:00
1 parent 0d2367431d
commit 634b63e16c
56 files changed
+941 -409

No files matched your search

+6 -6
View File
@@ -7,10 +7,10 @@ import 'package:flutter/foundation.dart';
import '../../core/data/models/watch_progress.dart';
import '../../core/data/repositories/watch_history_repository.dart';
/// Home browse channel, backed only by data already present in the catalogue.
/// 首页频道,只依赖目录中已有数据排序或过滤。
enum HomeChannel { hot, free, newReleases, ranking }
/// Local UI state for the browse-first home page.
/// 浏览型首页的本地 UI 状态。
class HomeBrowseLogic extends ChangeNotifier {
HomeBrowseLogic({required this.history}) {
unawaited(refreshRecent());
@@ -24,7 +24,7 @@ class HomeBrowseLogic extends ChangeNotifier {
bool loadingRecent = false;
bool _disposed = false;
/// First unfinished record with a real position.
/// 第一条未完成且存在真实播放位置的记录。
WatchProgress? get primaryRecord {
for (final record in recent) {
if (!record.finished && record.hasPosition) return record;
@@ -32,14 +32,14 @@ class HomeBrowseLogic extends ChangeNotifier {
return null;
}
/// Changes the selected browse channel without triggering network work.
/// 切换频道,不触发额外网络请求。
void selectChannel(HomeChannel value) {
if (channel == value) return;
channel = value;
_notify();
}
/// Reloads continuation data; signed-out reads simply return an empty list.
/// 重新加载续看数据;未登录时返回空列表。
Future<void> refreshRecent() async {
loadingRecent = true;
_notify();
@@ -53,7 +53,7 @@ class HomeBrowseLogic extends ChangeNotifier {
}
}
/// Refreshes continuation data when the home tab becomes visible again.
/// 首页重新可见时刷新续看数据。
void syncPage({required bool active, required bool wasActive}) {
if (active && !wasActive) unawaited(refreshRecent());
}
+2 -2
View File
@@ -20,7 +20,7 @@ import 'home_browse_logic.dart';
import 'widgets/home_browse_header.dart';
import 'widgets/home_drama_grid.dart';
/// Browse-first home page with channels, hero, continuation, and catalogue.
/// 浏览型首页:频道、Hero、继续观看与剧目目录。
class HomeBrowseView extends StatefulWidget {
const HomeBrowseView({
super.key,
@@ -214,7 +214,7 @@ class _HomeBrowseViewState extends State<HomeBrowseView> {
color: selected ? AppColors.blue : AppColors.border,
),
labelStyle: TextStyle(
color: selected ? AppColors.onPrimary : AppColors.muted,
color: selected ? AppColors.onPrimary : AppColors.onSurface,
fontSize: AppDimens.chipFont,
fontWeight: FontWeight.w700,
),
+15 -9
View File
@@ -8,7 +8,6 @@ import 'package:provider/provider.dart';
import '../../core/data/models/drama.dart';
import '../../core/data/services/catalog_controller.dart';
import '../../core/platform/haptics.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_bottom_nav.dart';
import '../../core/widgets/empty_state.dart';
@@ -32,8 +31,11 @@ class HomePage extends StatelessWidget {
if (!catalog.settled) return const SplashPage();
final l10n = AppLocalizations.of(context)!;
return Theme(
data: AppTheme.dark(),
child: _buildHome(context, catalog, l10n),
data: AppTheme.light(),
child: DecoratedBox(
decoration: const BoxDecoration(gradient: AppTheme.pageGradient),
child: _buildHome(context, catalog, l10n),
),
);
}
@@ -44,19 +46,19 @@ class HomePage extends StatelessWidget {
) {
if (catalog.loading) {
return const Scaffold(
backgroundColor: AppColors.ink,
backgroundColor: Colors.transparent,
body: Center(child: CircularProgressIndicator()),
);
}
if (catalog.error != null) {
return Scaffold(
backgroundColor: AppColors.ink,
backgroundColor: Colors.transparent,
body: HomeFailureView(onRetry: catalog.load),
);
}
if (catalog.dramas.isEmpty) {
return Scaffold(
backgroundColor: AppColors.ink,
backgroundColor: Colors.transparent,
body: EmptyState(
key: const Key('home-empty'),
icon: Icons.movie_outlined,
@@ -75,7 +77,7 @@ class HomePage extends StatelessWidget {
}
}
/// Home catalog failure with a single recovery action.
/// 首页目录加载失败与重试入口。
class HomeFailureView extends StatelessWidget {
const HomeFailureView({super.key, required this.onRetry});
@@ -93,7 +95,7 @@ class HomeFailureView extends StatelessWidget {
}
}
/// The four primary destinations share a single route and keep their state.
/// 四个根页面共享同一路由并保持各自状态。
class _HomeTabs extends StatefulWidget {
const _HomeTabs({super.key, required this.dramas});
@@ -136,7 +138,7 @@ class _HomeTabsState extends State<_HomeTabs> {
},
child: Scaffold(
key: logic.scaffoldKey,
backgroundColor: AppColors.ink,
backgroundColor: Colors.transparent,
body: _body(context),
bottomNavigationBar: logic.videoImmersive ? null : _nav(context, l10n),
),
@@ -193,6 +195,10 @@ class _HomeTabsState extends State<_HomeTabs> {
Widget _nav(BuildContext context, AppLocalizations l10n) {
return AppBottomNav(
barKey: const Key('home-tabs-bar'),
centerAction: AppBottomNavCenterAction(
label: l10n.createTab,
onTap: () => _selectTab(context, 2),
),
items: [
_navItem(context, l10n, 0),
_navItem(context, l10n, 1),
@@ -8,7 +8,7 @@ import '../../../l10n/app_localizations.dart';
const double _searchHeight = 36;
/// Fixed home header with catalogue search, weekday, and profile entry.
/// 首页固定顶栏:搜索、星期与头像入口。
class HomeBrowseHeader extends StatelessWidget {
const HomeBrowseHeader({
super.key,
+137 -40
View File
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import '../../../core/data/models/drama.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart';
@@ -12,8 +13,10 @@ const int _columns = 2;
const double _columnGap = 10;
const double _cardGap = 10;
const double _coverAspectRatio = 3 / 4;
const double _addSize = 28;
const double _addIconSize = 18;
/// Two-column drama grid used by the browse-first home page.
/// 首页浏览态使用的双列剧目网格。
class HomeDramaGrid extends StatelessWidget {
const HomeDramaGrid({
super.key,
@@ -57,7 +60,7 @@ class HomeDramaGrid extends StatelessWidget {
}
}
/// Home catalogue card with title, genre, free badge, episode count, and heat.
/// 首页剧卡:封面、角标、标题、题材、集数与热度。
class HomeDramaCard extends StatelessWidget {
const HomeDramaCard({
super.key,
@@ -72,21 +75,35 @@ class HomeDramaCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
key: Key('home-drama-card-${drama.id}'),
onTap: onTap,
borderRadius: BorderRadius.circular(AppDimens.coverRadius),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_cover(),
const SizedBox(height: AppDimens.spaceXs),
_title(),
const SizedBox(height: 2),
_genre(),
const SizedBox(height: AppDimens.spaceXs),
_metadata(),
],
return Container(
decoration: BoxDecoration(
color: AppColors.card,
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
boxShadow: const [AppTheme.cardShadow],
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
clipBehavior: Clip.antiAlias,
child: InkWell(
key: Key('home-drama-card-${drama.id}'),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(AppDimens.spaceSm),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_cover(),
const SizedBox(height: AppDimens.spaceSm),
_title(),
const SizedBox(height: AppDimens.spaceXs),
_genre(),
const SizedBox(height: AppDimens.spaceXs),
_metadata(),
],
),
),
),
),
);
}
@@ -104,7 +121,10 @@ class HomeDramaCard extends StatelessWidget {
showFallbackIcon: true,
fallbackAspectRatio: _coverAspectRatio,
),
if (_isFree) Positioned(top: 6, left: 6, child: _freeBadge()),
Positioned(top: 8, left: 8, child: _tagBadge()),
if (_lockedCount > 0)
Positioned(top: 8, right: 8, child: _lockedBadge()),
Positioned(right: 8, bottom: 8, child: _episodeBadge()),
],
),
);
@@ -115,17 +135,29 @@ class HomeDramaCard extends StatelessWidget {
drama.episodes.any((episode) => episode.isFree);
}
Widget _freeBadge() {
int get _lockedCount {
final explicit = drama.episodes.where((episode) => episode.locked).length;
if (explicit > 0) return explicit;
final inferred = drama.episodeCount - drama.freeEpisodes;
return inferred > 0 ? inferred : 0;
}
Widget _tagBadge() {
final label = _isFree ? l10n.free : l10n.originalTag;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
key: Key('home-drama-tag-${drama.id}'),
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.spaceSm,
vertical: 3,
),
decoration: BoxDecoration(
color: AppColors.blue,
borderRadius: BorderRadius.circular(6),
color: AppColors.badgePink,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: Text(
l10n.free,
label,
style: const TextStyle(
color: AppColors.onPrimary,
color: AppColors.badgePinkText,
fontSize: AppDimens.cardMeta,
fontWeight: FontWeight.w800,
),
@@ -133,6 +165,51 @@ class HomeDramaCard extends StatelessWidget {
);
}
Widget _lockedBadge() {
return Container(
key: Key('home-drama-locked-${drama.id}'),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: AppColors.overlayDark,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock_rounded, color: AppColors.onPrimary, size: 11),
const SizedBox(width: 2),
Text(
l10n.lockedEpisodeCount(_lockedCount),
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: 9,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _episodeBadge() {
return Container(
key: Key('home-drama-episodes-badge-${drama.id}'),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.badgeBeige,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: Text(
l10n.allEpisodesBadge(drama.episodeCount),
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 9,
fontWeight: FontWeight.w700,
),
),
);
}
Widget _title() {
return Text(
drama.title,
@@ -141,7 +218,7 @@ class HomeDramaCard extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: AppDimens.previewSectionTitle,
fontSize: AppDimens.cardTitle,
fontWeight: FontWeight.w800,
),
);
@@ -162,24 +239,44 @@ class HomeDramaCard extends StatelessWidget {
}
Widget _metadata() {
return Wrap(
spacing: AppDimens.spaceSm,
runSpacing: AppDimens.spaceXs,
return Row(
children: [
Text(
l10n.episodeCount(drama.episodeCount),
key: Key('home-drama-episode-count-${drama.id}'),
style: const TextStyle(
color: AppColors.subtle,
fontSize: AppDimens.cardViews,
Expanded(
child: Wrap(
spacing: AppDimens.spaceSm,
runSpacing: AppDimens.spaceXs,
children: [
Text(
l10n.episodeCount(drama.episodeCount),
key: Key('home-drama-episode-count-${drama.id}'),
style: const TextStyle(
color: AppColors.subtle,
fontSize: AppDimens.cardViews,
),
),
Text(
l10n.viewCountLabel(drama.viewCount),
key: Key('home-drama-views-${drama.id}'),
style: const TextStyle(
color: AppColors.subtle,
fontSize: AppDimens.cardViews,
),
),
],
),
),
Text(
l10n.viewCountLabel(drama.viewCount),
key: Key('home-drama-views-${drama.id}'),
style: const TextStyle(
color: AppColors.subtle,
fontSize: AppDimens.cardViews,
Container(
key: Key('home-drama-add-${drama.id}'),
width: _addSize,
height: _addSize,
decoration: const BoxDecoration(
color: AppColors.blue,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add_rounded,
color: AppColors.onPrimary,
size: _addIconSize,
),
),
],