merge: 合并 K39-A 二级页头统一
This commit is contained in:
commit
9864c8e102
26 files changed
+492
-427
No files matched your search
@@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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!,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||
import 'package:fruitshorts/core/theme/app_theme.dart';
|
||||
import 'package:fruitshorts/core/widgets/subpage_header.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('should_render_unified_light_header_dimensions', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: AppSubpageHeader(
|
||||
title: 'Title',
|
||||
backKey: const Key('sample-back'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSize(find.byKey(const Key('subpage-header'))).height, 52);
|
||||
expect(
|
||||
tester.getSize(find.byKey(const Key('sample-back'))),
|
||||
const Size(36, 36),
|
||||
);
|
||||
final title = tester.widget<Text>(find.byKey(const Key('subpage-title')));
|
||||
expect(title.style?.fontSize, 18);
|
||||
expect(title.style?.fontWeight, FontWeight.w800);
|
||||
expect(title.style?.color, AppColors.onSurface);
|
||||
final back = tester.getRect(find.byKey(const Key('sample-back')));
|
||||
final titleRect = tester.getRect(find.byKey(const Key('subpage-title')));
|
||||
expect(titleRect.left - back.right, closeTo(12, 0.5));
|
||||
});
|
||||
|
||||
testWidgets('should_use_64_height_when_subtitle_exists', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(
|
||||
body: AppSubpageHeader(
|
||||
title: 'Title',
|
||||
subtitle: 'Subtitle',
|
||||
backKey: const Key('sample-back'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSize(find.byKey(const Key('subpage-header'))).height, 64);
|
||||
expect(find.byKey(const Key('subpage-subtitle')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_use_transparent_dark_back_button', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: const Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: AppSubpageHeader(
|
||||
title: 'Title',
|
||||
backKey: Key('sample-back'),
|
||||
dark: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final material = tester.widget<Material>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('sample-back')),
|
||||
matching: find.byType(Material),
|
||||
),
|
||||
);
|
||||
final icon = tester.widget<Icon>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('sample-back')),
|
||||
matching: find.byIcon(Icons.chevron_left_rounded),
|
||||
),
|
||||
);
|
||||
expect(material.color, Colors.transparent);
|
||||
expect(icon.color, Colors.white);
|
||||
expect(icon.size, 20);
|
||||
});
|
||||
}
|
||||
@@ -29,6 +29,7 @@ void main() {
|
||||
await _pumpFilterPage(tester);
|
||||
|
||||
expect(find.byKey(const Key('filter-page')), findsOneWidget);
|
||||
_expectUnifiedHeader(tester, const Key('filter-back'));
|
||||
expect(
|
||||
find.byKey(const Key('filter-group-label-category')),
|
||||
findsOneWidget,
|
||||
@@ -184,6 +185,13 @@ final List<Drama> _filterDramas = [
|
||||
),
|
||||
];
|
||||
|
||||
void _expectUnifiedHeader(WidgetTester tester, Key backKey) {
|
||||
expect(tester.getSize(find.byKey(backKey)), const Size(36, 36));
|
||||
final title = tester.widget<Text>(find.byKey(const Key('subpage-title')));
|
||||
expect(title.style?.fontSize, 18);
|
||||
expect(title.style?.fontWeight, FontWeight.w800);
|
||||
}
|
||||
|
||||
Drama _drama({
|
||||
required String id,
|
||||
required String title,
|
||||
|
||||
@@ -29,6 +29,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('ranking-page')), findsOneWidget);
|
||||
_expectUnifiedHeader(tester, const Key('ranking-back'));
|
||||
expect(find.text('TOP 1'), findsOneWidget);
|
||||
expect(
|
||||
tester.widget<Text>(find.byKey(const Key('ranking-title-1'))).data,
|
||||
@@ -46,6 +47,15 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('new-releases-page')), findsOneWidget);
|
||||
_expectUnifiedHeader(tester, const Key('new-releases-back'));
|
||||
final newPage = tester.widget<Scaffold>(
|
||||
find.byKey(const Key('new-releases-page')),
|
||||
);
|
||||
expect(newPage.backgroundColor, AppColors.ink);
|
||||
final badge = tester.widget<Container>(
|
||||
find.byKey(const Key('new-releases-hero-badge')),
|
||||
);
|
||||
expect((badge.decoration! as BoxDecoration).color, AppColors.blue);
|
||||
expect(find.byKey(const Key('new-releases-hero')), findsOneWidget);
|
||||
expect(find.text('Vengeance Rising'), findsWidgets);
|
||||
expect(
|
||||
@@ -68,6 +78,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('reserve-page')), findsOneWidget);
|
||||
_expectUnifiedHeader(tester, const Key('reserve-back'));
|
||||
expect(find.byKey(const Key('reserve-week-strip')), findsOneWidget);
|
||||
for (var index = 0; index < 7; index++) {
|
||||
expect(find.byKey(Key('reserve-day-$index')), findsOneWidget);
|
||||
@@ -207,3 +218,10 @@ List<Drama> _rankDramas(int count) => [
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
void _expectUnifiedHeader(WidgetTester tester, Key backKey) {
|
||||
expect(tester.getSize(find.byKey(backKey)), const Size(36, 36));
|
||||
final title = tester.widget<Text>(find.byKey(const Key('subpage-title')));
|
||||
expect(title.style?.fontSize, 18);
|
||||
expect(title.style?.fontWeight, FontWeight.w800);
|
||||
}
|
||||
@@ -52,6 +52,13 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('settings-page')), findsOneWidget);
|
||||
expect(
|
||||
tester.getSize(find.byKey(const Key('settings-back'))),
|
||||
const Size(36, 36),
|
||||
);
|
||||
final title = tester.widget<Text>(find.byKey(const Key('subpage-title')));
|
||||
expect(title.style?.fontSize, 18);
|
||||
expect(title.style?.fontWeight, FontWeight.w800);
|
||||
expect(find.byKey(const Key('settings-notices')), findsOneWidget);
|
||||
expect(find.byKey(const Key('server-endpoint-setting')), findsNothing);
|
||||
expect(find.text('Server address'), findsNothing);
|
||||
|
||||
@@ -52,6 +52,13 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('notices-page')), findsOneWidget);
|
||||
expect(
|
||||
tester.getSize(find.byKey(const Key('notices-back'))),
|
||||
const Size(36, 36),
|
||||
);
|
||||
final title = tester.widget<Text>(find.byKey(const Key('subpage-title')));
|
||||
expect(title.style?.fontSize, 18);
|
||||
expect(title.style?.fontWeight, FontWeight.w800);
|
||||
expect(find.byKey(const Key('auth-gate-panel')), findsNothing);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user