feat(flutter): 统一二级页头并恢复新剧暖色

This commit is contained in:
Zhou Weidong committed 2026-10-20 15:55:12 +08:00
1 parent 7b2970d5c5
commit 1c6c92170d
26 files changed
+492 -427

No files matched your search

+5 -40
View File
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import 'subpage_header.dart';
/// 二级页面通用骨架:返回栏 + 标题/副标题 + 可滚动内容,无底部导航。
class PlainPageScaffold extends StatelessWidget {
@@ -30,47 +31,11 @@ class PlainPageScaffold extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(6, 4, 14, 0),
child: Row(
children: [
IconButton(
key: backKey,
tooltip: MaterialLocalizations.of(
context,
).backButtonTooltip,
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(
Icons.chevron_left_rounded,
color: AppColors.onSurface,
size: 26,
),
),
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 20,
fontWeight: FontWeight.w800,
),
),
),
],
),
AppSubpageHeader(
title: title,
subtitle: subtitle,
backKey: backKey,
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Text(
subtitle!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: AppColors.muted, fontSize: 12),
),
),
Expanded(child: child),
],
),
+130
View File
@@ -0,0 +1,130 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
const double _height = 52;
const double _heightWithSubtitle = 64;
const double _horizontal = 14;
const double _backSize = 36;
const double _backGap = 12;
const double _titleFontSize = 18;
const double _subtitleFontSize = 12;
const double _actionGap = 8;
const double _iconSize = 20;
/// 二级页面统一返回按钮,浅色页使用 card 圆底,深色页使用透明底。
class AppSubpageBackButton extends StatelessWidget {
const AppSubpageBackButton({
super.key,
required this.backKey,
this.dark = false,
this.onBack,
});
final Key backKey;
final bool dark;
final VoidCallback? onBack;
@override
Widget build(BuildContext context) {
return SizedBox(
key: backKey,
width: _backSize,
height: _backSize,
child: Tooltip(
message: MaterialLocalizations.of(context).backButtonTooltip,
child: Material(
color: dark ? Colors.transparent : AppColors.card,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onBack ?? () => Navigator.of(context).maybePop(),
child: Icon(
Icons.chevron_left_rounded,
color: dark ? Colors.white : AppColors.onSurface,
size: _iconSize,
),
),
),
),
);
}
}
/// 二级页面统一页头:返回按钮、左对齐标题、可选副标题与右侧操作。
class AppSubpageHeader extends StatelessWidget {
const AppSubpageHeader({
super.key,
required this.title,
required this.backKey,
this.subtitle,
this.actions,
this.onBack,
this.dark = false,
});
final String title;
final Key backKey;
final String? subtitle;
final Widget? actions;
final VoidCallback? onBack;
final bool dark;
@override
Widget build(BuildContext context) {
final foreground = dark ? Colors.white : AppColors.onSurface;
final secondary = dark ? Colors.white70 : AppColors.muted;
return SizedBox(
key: const Key('subpage-header'),
height: subtitle == null ? _height : _heightWithSubtitle,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: _horizontal),
child: Row(
children: [
AppSubpageBackButton(backKey: backKey, dark: dark, onBack: onBack),
const SizedBox(width: _backGap),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
key: const Key('subpage-title'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: foreground,
fontSize: _titleFontSize,
fontWeight: FontWeight.w800,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
key: const Key('subpage-subtitle'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: secondary,
fontSize: _subtitleFontSize,
fontWeight: FontWeight.w500,
),
),
],
],
),
),
if (actions != null) ...[
const SizedBox(width: _actionGap),
actions!,
],
],
),
),
);
}
}
+11 -17
View File
@@ -12,6 +12,7 @@ import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import 'account_bindings.dart';
import 'account_deactivate_logic.dart';
@@ -51,23 +52,16 @@ class _AccountDeactivatePageState extends State<AccountDeactivatePage> {
final canShow = signedIn || _logic.scheduledAt != null;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('account-deactivate-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onPrimary,
title: Text(l10n.accountDeactivate),
),
body: SafeArea(
top: false,
child: canShow
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
child: PlainPageScaffold(
pageKey: const Key('account-deactivate-page'),
backKey: const Key('account-deactivate-back'),
title: l10n.accountDeactivate,
child: canShow
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
);
}
+11 -17
View File
@@ -15,6 +15,7 @@ import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import 'account_bindings.dart';
import 'account_devices_logic.dart';
@@ -51,23 +52,16 @@ class _AccountDevicesPageState extends State<AccountDevicesPage> {
final signedIn = context.watch<AuthController>().isSignedIn;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('account-devices-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onPrimary,
title: Text(l10n.accountDevices),
),
body: SafeArea(
top: false,
child: signedIn
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
child: PlainPageScaffold(
pageKey: const Key('account-devices-page'),
backKey: const Key('account-devices-back'),
title: l10n.accountDevices,
child: signedIn
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
);
}
+11 -17
View File
@@ -15,6 +15,7 @@ import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import 'account_bindings.dart';
import 'account_export_logic.dart';
@@ -52,23 +53,16 @@ class _AccountExportPageState extends State<AccountExportPage> {
final signedIn = context.watch<AuthController>().isSignedIn;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('account-export-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onPrimary,
title: Text(l10n.accountDataExport),
),
body: SafeArea(
top: false,
child: signedIn
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
child: PlainPageScaffold(
pageKey: const Key('account-export-page'),
backKey: const Key('account-export-back'),
title: l10n.accountDataExport,
child: signedIn
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
);
}
+11 -17
View File
@@ -15,6 +15,7 @@ import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../core/widgets/remote_image.dart';
import '../../l10n/app_localizations.dart';
import 'account_bindings.dart';
@@ -72,23 +73,16 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
final signedIn = context.watch<AuthController>().isSignedIn;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('account-profile-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onPrimary,
title: Text(l10n.accountProfile),
),
body: SafeArea(
top: false,
child: signedIn
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
child: PlainPageScaffold(
pageKey: const Key('account-profile-page'),
backKey: const Key('account-profile-back'),
title: l10n.accountProfile,
child: signedIn
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
);
}
+6 -5
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
const double _cardHeight = 196;
@@ -343,11 +344,11 @@ class _CreatePlaceholderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
key: const Key('create-placeholder-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(title: Text(title)),
body: Center(
return PlainPageScaffold(
pageKey: const Key('create-placeholder-page'),
backKey: const Key('create-placeholder-back'),
title: title,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
+19 -26
View File
@@ -3,9 +3,9 @@
import 'package:flutter/material.dart';
import '../../core/data/models/drama.dart';
import '../../core/theme/app_colors.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';
@@ -28,32 +28,25 @@ class CategoryDramasPage extends StatelessWidget {
final l10n = AppLocalizations.of(context)!;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('category-dramas-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onSurface,
title: Text(title),
),
body: SafeArea(
top: false,
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: 12, bottom: 28),
child: DramaGrid(
l10n: l10n,
dramas: dramas,
onOpen: (drama) => _open(context, drama),
),
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),
),
),
),
),
);
}
+8 -14
View File
@@ -18,6 +18,7 @@ import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import '../player/player_view.dart';
import 'downloads_binding.dart';
@@ -55,20 +56,13 @@ class _DownloadsPageState extends State<DownloadsPage> {
final signedIn = context.watch<AuthController>().isSignedIn;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('downloads-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: Colors.white,
title: Text(l10n.downloads),
),
body: SafeArea(
top: false,
child: signedIn
? _DownloadsBody(downloads: context.watch<DownloadController>())
: const AuthGatePanel(),
),
child: PlainPageScaffold(
pageKey: const Key('downloads-page'),
backKey: const Key('downloads-back'),
title: l10n.downloads,
child: signedIn
? _DownloadsBody(downloads: context.watch<DownloadController>())
: const AuthGatePanel(),
),
);
}
+22 -24
View File
@@ -20,6 +20,7 @@ import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/download_action.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/subpage_header.dart';
import '../../l10n/app_localizations.dart';
import '../interaction/widgets/report_dialog.dart';
import 'drama_detail_binding.dart';
@@ -145,24 +146,14 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
);
}
Widget _loading(AppLocalizations l10n) => Scaffold(
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.surfaceTop,
foregroundColor: AppColors.onSurface,
title: Text(localizedDramaTitle(l10n, widget.drama)),
),
body: const Center(child: CircularProgressIndicator()),
Widget _loading(AppLocalizations l10n) => _statePage(
localizedDramaTitle(l10n, widget.drama),
const Center(child: CircularProgressIndicator()),
);
Widget _failed(AppLocalizations l10n) => Scaffold(
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.surfaceTop,
foregroundColor: AppColors.onSurface,
title: Text(localizedDramaTitle(l10n, widget.drama)),
),
body: Center(
Widget _failed(AppLocalizations l10n) => _statePage(
localizedDramaTitle(l10n, widget.drama),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
@@ -181,14 +172,9 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
),
);
Widget _empty(AppLocalizations l10n, Drama drama) => Scaffold(
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.surfaceTop,
foregroundColor: AppColors.onSurface,
title: Text(localizedDramaTitle(l10n, drama)),
),
body: EmptyState(
Widget _empty(AppLocalizations l10n, Drama drama) => _statePage(
localizedDramaTitle(l10n, drama),
EmptyState(
key: const Key('drama-detail-empty'),
icon: Icons.video_library_outlined,
title: l10n.noEpisodesYet,
@@ -198,6 +184,18 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
),
);
Widget _statePage(String title, Widget child) => Scaffold(
backgroundColor: AppColors.ink,
body: SafeArea(
child: Column(
children: [
AppSubpageHeader(title: title, backKey: const Key('detail-back')),
Expanded(child: child),
],
),
),
);
Widget? _resume(AppLocalizations l10n) {
final fraction = _continueFraction;
final progressError = logic.progressError;
@@ -6,6 +6,7 @@ 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 '../../../core/widgets/subpage_header.dart';
import '../../../l10n/app_localizations.dart';
const double _horizontal = 14;
@@ -80,11 +81,10 @@ class DramaDetailHeader extends StatelessWidget {
Widget _actions(BuildContext context) {
return Row(
children: [
_CircleAction(
actionKey: const Key('detail-back'),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: Icons.chevron_left_rounded,
onPressed: actions.onBack,
AppSubpageBackButton(
backKey: const Key('detail-back'),
dark: true,
onBack: actions.onBack,
),
const Spacer(),
_CircleAction(
+8 -13
View File
@@ -15,6 +15,7 @@ import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/library_drama_tile.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import '../drama_detail/drama_detail_view.dart';
import 'favorites_binding.dart';
@@ -52,19 +53,13 @@ class _FavoritesPageState extends State<FavoritesPage> {
final signedIn = context.watch<AuthController>().isSignedIn;
return Theme(
data: AppTheme.light(),
child: Scaffold(
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: Colors.white,
title: Text(l10n.myFavorites),
),
body: SafeArea(
top: false,
child: signedIn
? _FavoritesBody(favorites: context.watch<FavoriteController>())
: const AuthGatePanel(),
),
child: PlainPageScaffold(
pageKey: const Key('favorites-page'),
backKey: const Key('favorites-back'),
title: l10n.myFavorites,
child: signedIn
? _FavoritesBody(favorites: context.watch<FavoriteController>())
: const AuthGatePanel(),
),
);
}
+5 -39
View File
@@ -9,6 +9,7 @@ import '../../core/data/services/catalog_controller.dart';
import '../../core/data/services/category_controller.dart';
import '../../core/theme/app_colors.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/subpage_header.dart';
import '../../l10n/app_localizations.dart';
import 'widgets/filter_result_card.dart';
@@ -64,7 +65,10 @@ class _FilterPageState extends State<FilterPage> {
body: SafeArea(
child: Column(
children: [
_topBar(l10n),
AppSubpageHeader(
title: l10n.filter,
backKey: const Key('filter-back'),
),
Expanded(
child: CustomScrollView(
slivers: [
@@ -123,44 +127,6 @@ class _FilterPageState extends State<FilterPage> {
);
}
Widget _topBar(AppLocalizations l10n) {
return SizedBox(
height: 52,
child: Row(
children: [
SizedBox(
key: const Key('filter-back'),
width: 48,
height: 48,
child: IconButton(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () => Navigator.of(context).pop(),
padding: EdgeInsets.zero,
icon: const Icon(
Icons.chevron_left_rounded,
color: AppColors.onSurface,
size: 26,
),
),
),
Expanded(
child: Center(
child: Text(
l10n.filter,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 18,
fontWeight: FontWeight.w800,
),
),
),
),
const SizedBox(width: 48),
],
),
);
}
Widget _groups(
AppLocalizations l10n,
List<String> categories,
+6 -5
View File
@@ -14,6 +14,7 @@ import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_bottom_nav.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import '../downloads/downloads_page.dart';
import '../favorites/favorites_page.dart';
@@ -364,11 +365,11 @@ class _MinePlaceholderPage extends StatelessWidget {
MinePlaceholderKind.creator => l10n.creatorCenter,
MinePlaceholderKind.help => l10n.helpFeedback,
};
return Scaffold(
key: const Key('mine-placeholder-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(title: Text(title)),
body: Center(
return PlainPageScaffold(
pageKey: const Key('mine-placeholder-page'),
backKey: const Key('mine-placeholder-back'),
title: title,
child: Center(
child: Text(
l10n.comingSoon,
style: const TextStyle(
+59 -75
View File
@@ -11,16 +11,18 @@ import '../../core/data/services/catalog_controller.dart';
import '../../core/formatting/count_format.dart';
import '../../core/theme/app_colors.dart';
import '../../core/widgets/remote_image.dart';
import '../../core/widgets/subpage_header.dart';
import '../../l10n/app_localizations.dart';
import '../player/open_player.dart';
const int _moreLimit = 12;
const int _filmHoles = 8;
const Color _premiereTop = Color(0xFF1F1B16);
const Color _premiereBottom = Color(0xFF2E2119);
const Color _filmHole = Color(0x33FFFFFF);
const double _cardPadding = 8;
const double _cardWidth = 124;
const double _rowHeight = 204;
const double _cardRadius = 12;
/// 新剧页:暗色首映厅,今日首映大卡 + 横向更多新剧。
/// 新剧页:暖色首映厅,今日首映大卡 + 横向更多新剧。
class NewReleasesPage extends StatelessWidget {
const NewReleasesPage({super.key});
@@ -32,29 +34,35 @@ class NewReleasesPage extends StatelessWidget {
final hero = dramas.isEmpty ? null : dramas.first;
final more = dramas.skip(1).take(_moreLimit).toList();
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
value: SystemUiOverlayStyle.dark,
child: Scaffold(
key: const Key('new-releases-page'),
backgroundColor: AppColors.ink,
body: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [_premiereTop, _premiereBottom],
colors: [AppColors.surfaceTop, AppColors.ink],
),
),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_header(context, l10n, dramas.length),
AppSubpageHeader(
title: l10n.newReleasesTitle,
subtitle: l10n.newReleasesSubtitle,
backKey: const Key('new-releases-back'),
actions: _countChip(l10n, dramas.length),
),
Expanded(
child: hero == null
? Center(
child: Text(
l10n.noStories,
key: const Key('new-releases-empty'),
style: const TextStyle(color: Colors.white70),
style: const TextStyle(color: AppColors.muted),
),
)
: ListView(
@@ -73,7 +81,7 @@ class NewReleasesPage extends StatelessWidget {
_moreHeader(l10n),
const SizedBox(height: 10),
SizedBox(
height: 196,
height: _rowHeight,
child: ListView.separated(
key: const Key('new-releases-row'),
scrollDirection: Axis.horizontal,
@@ -103,63 +111,21 @@ class NewReleasesPage extends StatelessWidget {
);
}
Widget _header(BuildContext context, AppLocalizations l10n, int count) {
return Padding(
padding: const EdgeInsets.fromLTRB(6, 4, 16, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconButton(
key: const Key('new-releases-back'),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(
Icons.chevron_left_rounded,
color: Colors.white,
size: 26,
),
),
Expanded(
child: Text(
l10n.newReleasesTitle,
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.w900,
),
),
),
Container(
key: const Key('new-releases-count'),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: AppColors.blue,
borderRadius: BorderRadius.circular(999),
),
child: Text(
l10n.newReleasesCount(count),
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
),
],
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
l10n.newReleasesSubtitle,
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
),
],
Widget _countChip(AppLocalizations l10n, int count) {
return Container(
key: const Key('new-releases-count'),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppColors.badgeBeige,
borderRadius: BorderRadius.circular(999),
),
child: Text(
l10n.newReleasesCount(count),
style: const TextStyle(
color: AppColors.blue,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
);
}
@@ -286,7 +252,7 @@ class NewReleasesPage extends StatelessWidget {
child: Text(
l10n.rankingNew,
style: const TextStyle(
color: Colors.white,
color: AppColors.onSurface,
fontSize: 16,
fontWeight: FontWeight.w800,
),
@@ -294,7 +260,7 @@ class NewReleasesPage extends StatelessWidget {
),
Text(
l10n.seeAll,
style: const TextStyle(color: Colors.white54, fontSize: 12),
style: const TextStyle(color: AppColors.subtle, fontSize: 12),
),
],
),
@@ -310,13 +276,25 @@ class NewReleasesPage extends StatelessWidget {
return GestureDetector(
key: Key('new-release-card-${drama.id}'),
onTap: () => unawaited(playDramaFromCatalog(context, drama)),
child: SizedBox(
width: 108,
child: Container(
width: _cardWidth,
padding: const EdgeInsets.all(_cardPadding),
decoration: BoxDecoration(
color: AppColors.card,
borderRadius: BorderRadius.circular(_cardRadius),
boxShadow: const [
BoxShadow(
color: AppColors.cardShadow,
blurRadius: 12,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 108,
height: 144,
@@ -348,7 +326,7 @@ class NewReleasesPage extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
color: AppColors.onSurface,
fontSize: 13,
fontWeight: FontWeight.w700,
),
@@ -356,7 +334,7 @@ class NewReleasesPage extends StatelessWidget {
const SizedBox(height: 2),
Text(
_releaseDate(index),
style: const TextStyle(color: Colors.white54, fontSize: 12),
style: const TextStyle(color: AppColors.subtle, fontSize: 12),
),
],
),
@@ -407,7 +385,13 @@ class _FilmStrip extends StatelessWidget {
return Container(
key: const Key('new-releases-film'),
height: 22,
color: const Color(0x1A000000),
decoration: const BoxDecoration(
color: AppColors.raised,
border: Border(
top: BorderSide(color: AppColors.border),
bottom: BorderSide(color: AppColors.border),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [_holes(), _holes()],
@@ -424,7 +408,7 @@ class _FilmStrip extends StatelessWidget {
width: 6,
height: 8,
decoration: BoxDecoration(
color: _filmHole,
color: AppColors.border,
borderRadius: BorderRadius.circular(2),
),
),
+8 -14
View File
@@ -14,6 +14,7 @@ import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.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';
/// Anonymous system notice list with pagination and detail dialog.
@@ -45,20 +46,13 @@ class _NoticesPageState extends State<NoticesPage> {
final l10n = AppLocalizations.of(context)!;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('notices-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onPrimary,
title: Text(l10n.notices),
),
body: SafeArea(
top: false,
child: ListenableBuilder(
listenable: _controller,
builder: (context, _) => _body(l10n),
),
child: PlainPageScaffold(
pageKey: const Key('notices-page'),
backKey: const Key('notices-back'),
title: l10n.notices,
child: ListenableBuilder(
listenable: _controller,
builder: (context, _) => _body(l10n),
),
),
);
+6 -43
View File
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/widgets/subpage_header.dart';
import '../../l10n/app_localizations.dart';
import '../new_releases/new_releases_page.dart';
@@ -43,7 +44,11 @@ class _ReservePageState extends State<ReservePage> {
child: ListView(
padding: const EdgeInsets.fromLTRB(0, 4, 0, 24),
children: [
_header(context, l10n),
AppSubpageHeader(
title: l10n.reserveTitle,
subtitle: l10n.reserveSubtitle,
backKey: const Key('reserve-back'),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: _weekStrip(l10n),
@@ -62,48 +67,6 @@ class _ReservePageState extends State<ReservePage> {
);
}
Widget _header(BuildContext context, AppLocalizations l10n) {
return Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconButton(
key: const Key('reserve-back'),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(
Icons.chevron_left_rounded,
color: AppColors.onSurface,
size: 26,
),
),
Expanded(
child: Text(
l10n.reserveTitle,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 24,
fontWeight: FontWeight.w800,
),
),
),
],
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
l10n.reserveSubtitle,
style: const TextStyle(color: AppColors.muted, fontSize: 12),
),
),
],
),
);
}
Widget _weekStrip(AppLocalizations l10n) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
+15 -21
View File
@@ -9,6 +9,7 @@ import '../../core/data/services/auth_controller.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart';
import '../account/account_entry_card.dart';
import '../notices/notices_page.dart';
@@ -23,28 +24,21 @@ class SettingsPage extends StatelessWidget {
final auth = context.watch<AuthController>();
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('settings-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onSurface,
title: Text(l10n.settings),
),
body: SafeArea(
top: false,
child: ListView(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 28),
children: [
if (auth.isSignedIn) ...[
const AccountEntryCard(),
const SizedBox(height: 12),
],
_noticesTile(context, l10n),
const SizedBox(height: 24),
_authAction(context, l10n, auth),
child: PlainPageScaffold(
pageKey: const Key('settings-page'),
backKey: const Key('settings-back'),
title: l10n.settings,
child: ListView(
padding: const EdgeInsets.fromLTRB(14, 8, 14, 28),
children: [
if (auth.isSignedIn) ...[
const AccountEntryCard(),
const SizedBox(height: 12),
],
),
_noticesTile(context, l10n),
const SizedBox(height: 24),
_authAction(context, l10n, auth),
],
),
),
);
+8 -14
View File
@@ -13,6 +13,7 @@ import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.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';
/// 商品货架:金币包与会员档位共用一套真实接口渲染。
@@ -57,20 +58,13 @@ class _StorePageState extends State<StorePage> {
final isVip = widget.type == StoreController.vipType;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('store-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onSurface,
title: Text(isVip ? l10n.vipTitle : l10n.rechargeTitle),
),
body: SafeArea(
top: false,
child: ListenableBuilder(
listenable: controller,
builder: (context, _) => _body(l10n, isVip),
),
child: PlainPageScaffold(
pageKey: const Key('store-page'),
backKey: const Key('store-back'),
title: isVip ? l10n.vipTitle : l10n.rechargeTitle,
child: ListenableBuilder(
listenable: controller,
builder: (context, _) => _body(l10n, isVip),
),
),
);
+6 -9
View File
@@ -13,6 +13,7 @@ import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../store/recharge_page.dart';
import '../store/vip_page.dart';
import '../../l10n/app_localizations.dart';
@@ -45,15 +46,11 @@ class _WalletPageState extends State<WalletPage> {
final wallet = context.watch<WalletController>();
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('wallet-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onSurface,
title: Text(l10n.wallet),
),
body: SafeArea(top: false, child: _body(l10n, wallet)),
child: PlainPageScaffold(
pageKey: const Key('wallet-page'),
backKey: const Key('wallet-back'),
title: l10n.wallet,
child: _body(l10n, wallet),
),
);
}
@@ -19,6 +19,7 @@ import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../core/widgets/remote_image.dart';
import '../../l10n/app_localizations.dart';
import '../drama_detail/drama_detail_view.dart';
@@ -34,18 +35,11 @@ class WatchRecordsPage extends StatelessWidget {
final l10n = AppLocalizations.of(context)!;
return Theme(
data: AppTheme.light(),
child: Scaffold(
key: const Key('watch-records-page'),
backgroundColor: AppColors.ink,
appBar: AppBar(
backgroundColor: AppColors.ink,
foregroundColor: Colors.white,
title: Text(l10n.watchHistory),
),
body: SafeArea(
top: false,
child: signedIn ? const _WatchRecordsBody() : const AuthGatePanel(),
),
child: PlainPageScaffold(
pageKey: const Key('watch-records-page'),
backKey: const Key('watch-records-back'),
title: l10n.watchHistory,
child: signedIn ? const _WatchRecordsBody() : const AuthGatePanel(),
),
);
}