diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index 0da82a6..809af25 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -10,6 +10,7 @@ abstract final class AppDimens { static const double pageHorizontal = 16; static const double cardRadius = 16; + static const double authCardRadius = 20; static const double cardPadding = 16; static const double innerRadius = 12; static const double innerPadding = 12; diff --git a/lib/core/widgets/app_bottom_nav.dart b/lib/core/widgets/app_bottom_nav.dart index c35a033..ad481da 100644 --- a/lib/core/widgets/app_bottom_nav.dart +++ b/lib/core/widgets/app_bottom_nav.dart @@ -30,15 +30,18 @@ class AppBottomNav extends StatelessWidget { super.key, required this.items, this.backgroundColor = AppColors.bar, + this.barKey, }); final List items; final Color backgroundColor; + final Key? barKey; @override Widget build(BuildContext context) => SafeArea( top: false, child: Container( + key: barKey, height: AppDimens.navHeight, decoration: BoxDecoration( color: backgroundColor, @@ -81,7 +84,7 @@ class _NavDestination extends StatelessWidget { ), decoration: BoxDecoration( color: item.selected ? AppColors.blue : Colors.transparent, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Icon( item.selected ? item.selectedIcon : item.icon, diff --git a/lib/core/widgets/empty_state.dart b/lib/core/widgets/empty_state.dart index 6315ba1..40c2e62 100644 --- a/lib/core/widgets/empty_state.dart +++ b/lib/core/widgets/empty_state.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../theme/app_colors.dart'; +import '../theme/app_dimens.dart'; const double _regularIconSize = 48; const double _compactIconSize = 32; @@ -139,7 +140,7 @@ class _EmptyCopy extends StatelessWidget { textAlign: TextAlign.center, style: const TextStyle( color: AppColors.muted, - fontSize: 12.5, + fontSize: AppDimens.caption, height: 1.45, ), ), @@ -151,11 +152,14 @@ class _EmptyCopy extends StatelessWidget { key: actionKey ?? const Key('empty-state-action'), onPressed: onAction, style: TextButton.styleFrom( - foregroundColor: scheme.primary, + foregroundColor: AppColors.blueSoft, minimumSize: const Size(44, _actionMinHeight), padding: const EdgeInsets.symmetric(horizontal: 14), ), - child: Text(actionLabel!, style: const TextStyle(fontSize: 13)), + child: Text( + actionLabel!, + style: const TextStyle(fontSize: AppDimens.body), + ), ), ], ], diff --git a/lib/core/widgets/library_drama_tile.dart b/lib/core/widgets/library_drama_tile.dart index d63ee99..2c715ff 100644 --- a/lib/core/widgets/library_drama_tile.dart +++ b/lib/core/widgets/library_drama_tile.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import '../data/models/drama.dart'; import '../theme/app_colors.dart'; +import '../theme/app_dimens.dart'; import 'remote_image.dart'; /// 收藏等账号列表复用的剧集卡片,视觉与"我的"页列表保持一致。 @@ -23,16 +24,16 @@ class LibraryDramaTile extends StatelessWidget { Widget build(BuildContext context) { return Material( color: AppColors.card, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Padding( - padding: const EdgeInsets.all(9), + padding: const EdgeInsets.all(AppDimens.innerPadding), child: Row( children: [ ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), child: RemoteImage(drama.posterUrl, width: 74, height: 103), ), const SizedBox(width: 11), @@ -46,7 +47,7 @@ class LibraryDramaTile extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, - fontSize: 15, + fontSize: AppDimens.listTitle, fontWeight: FontWeight.w700, ), ), @@ -57,7 +58,7 @@ class LibraryDramaTile extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.muted, - fontSize: 12, + fontSize: AppDimens.caption, ), ), ], diff --git a/lib/pages/account/account_deactivate_page.dart b/lib/pages/account/account_deactivate_page.dart index 61c01d4..cf1799f 100644 --- a/lib/pages/account/account_deactivate_page.dart +++ b/lib/pages/account/account_deactivate_page.dart @@ -9,6 +9,7 @@ import '../../core/data/repositories/account_repository.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/formatting/localized_date_time.dart'; 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 '../../l10n/app_localizations.dart'; @@ -58,12 +59,15 @@ class _AccountDeactivatePageState extends State { foregroundColor: AppColors.onPrimary, title: Text(l10n.accountDeactivate), ), - body: canShow - ? ListenableBuilder( - listenable: _logic, - builder: (context, _) => _body(l10n), - ) - : const AuthGatePanel(), + body: SafeArea( + top: false, + child: canShow + ? ListenableBuilder( + listenable: _logic, + builder: (context, _) => _body(l10n), + ) + : const AuthGatePanel(), + ), ), ); } @@ -90,7 +94,7 @@ class _AccountDeactivatePageState extends State { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: errorColor.withValues(alpha: 0.10), - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: errorColor.withValues(alpha: 0.35)), ), child: Text( @@ -105,7 +109,7 @@ class _AccountDeactivatePageState extends State { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), ), child: Text( l10n.accountScheduledFor( @@ -161,7 +165,10 @@ class _AccountDeactivatePageState extends State { Widget _errorText(String value) => Text( value, - style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: AppDimens.caption, + ), ); Future _confirm() async { diff --git a/lib/pages/account/account_devices_page.dart b/lib/pages/account/account_devices_page.dart index 9be51e7..b64be9c 100644 --- a/lib/pages/account/account_devices_page.dart +++ b/lib/pages/account/account_devices_page.dart @@ -11,6 +11,7 @@ import '../../core/data/services/auth_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; 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/empty_state.dart'; @@ -58,12 +59,15 @@ class _AccountDevicesPageState extends State { foregroundColor: AppColors.onPrimary, title: Text(l10n.accountDevices), ), - body: signedIn - ? ListenableBuilder( - listenable: _logic, - builder: (context, _) => _body(l10n), - ) - : const AuthGatePanel(), + body: SafeArea( + top: false, + child: signedIn + ? ListenableBuilder( + listenable: _logic, + builder: (context, _) => _body(l10n), + ) + : const AuthGatePanel(), + ), ), ); } @@ -109,10 +113,10 @@ class _DeviceTile extends StatelessWidget { Widget build(BuildContext context) { return Container( key: ValueKey('account-device-${device.id}'), - padding: const EdgeInsets.all(14), + padding: const EdgeInsets.all(AppDimens.cardPadding), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.outlineVariantDark), ), child: Column( @@ -131,7 +135,7 @@ class _DeviceTile extends StatelessWidget { ? device.platform : device.deviceModel, style: const TextStyle( - fontSize: 15, + fontSize: AppDimens.listTitle, fontWeight: FontWeight.w700, ), ), @@ -164,14 +168,17 @@ class _DeviceTile extends StatelessWidget { Expanded( child: Text( label, - style: const TextStyle(color: AppColors.muted, fontSize: 12), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ), Flexible( child: Text( value.isEmpty ? '-' : value, textAlign: TextAlign.right, - style: const TextStyle(fontSize: 12), + style: const TextStyle(fontSize: AppDimens.caption), ), ), ], @@ -188,20 +195,19 @@ class _StatusChip extends StatelessWidget { @override Widget build(BuildContext context) { final active = status.toLowerCase() == 'active'; - final scheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: active - ? scheme.tertiary.withValues(alpha: 0.16) + ? AppColors.gold.withValues(alpha: 0.16) : AppColors.raised, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Text( active ? l10n.accountDeviceActive : l10n.accountDeviceInactive, style: TextStyle( - color: active ? scheme.tertiary : AppColors.muted, - fontSize: 11, + color: active ? AppColors.gold : AppColors.muted, + fontSize: AppDimens.caption, ), ), ); diff --git a/lib/pages/account/account_entry_card.dart b/lib/pages/account/account_entry_card.dart index 65c687e..e12dc83 100644 --- a/lib/pages/account/account_entry_card.dart +++ b/lib/pages/account/account_entry_card.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_dimens.dart'; import '../../l10n/app_localizations.dart'; import 'account_deactivate_page.dart'; import 'account_devices_page.dart'; import 'account_export_page.dart'; import 'account_profile_page.dart'; -const _accountCardHeight = 48.0; const _accountTitleWidth = 64.0; /// 我的页账号入口卡;只在登录态由父页面展示。 @@ -25,7 +25,7 @@ class AccountEntryCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), clipBehavior: Clip.antiAlias, child: SizedBox( - height: _accountCardHeight, + height: AppDimens.accountCardHeight, child: Row( children: [ SizedBox( @@ -35,7 +35,7 @@ class AccountEntryCard extends StatelessWidget { child: Text( l10n.accountCenter, style: const TextStyle( - fontSize: 12, + fontSize: AppDimens.caption, fontWeight: FontWeight.w800, ), ), @@ -109,14 +109,21 @@ class AccountEntryCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, color: AppColors.blueSoft, size: 16), + Icon( + icon, + color: AppColors.blueSoft, + size: AppDimens.accountIcon, + ), const SizedBox(height: 2), Text( title, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, - style: const TextStyle(fontSize: 9.5, height: 1.05), + style: const TextStyle( + fontSize: AppDimens.accountLabel, + height: 1.15, + ), ), ], ), diff --git a/lib/pages/account/account_export_page.dart b/lib/pages/account/account_export_page.dart index 56c0396..d17cde7 100644 --- a/lib/pages/account/account_export_page.dart +++ b/lib/pages/account/account_export_page.dart @@ -11,6 +11,7 @@ import '../../core/data/repositories/account_repository.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/formatting/error_text.dart'; 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/empty_state.dart'; @@ -59,12 +60,15 @@ class _AccountExportPageState extends State { foregroundColor: AppColors.onPrimary, title: Text(l10n.accountDataExport), ), - body: signedIn - ? ListenableBuilder( - listenable: _logic, - builder: (context, _) => _body(l10n), - ) - : const AuthGatePanel(), + body: SafeArea( + top: false, + child: signedIn + ? ListenableBuilder( + listenable: _logic, + builder: (context, _) => _body(l10n), + ) + : const AuthGatePanel(), + ), ), ); } @@ -106,7 +110,10 @@ class _AccountExportPageState extends State { const SizedBox(height: 4), Text( '${l10n.accountUid}: ${data.profile.uid}', - style: const TextStyle(color: AppColors.muted, fontSize: 12), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ], ); @@ -115,7 +122,10 @@ class _AccountExportPageState extends State { children: [ Text( l10n.accountCredentials, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), + style: const TextStyle( + fontSize: AppDimens.listTitle, + fontWeight: FontWeight.w700, + ), ), for (final credential in data.credentials) ListTile( @@ -128,7 +138,10 @@ class _AccountExportPageState extends State { subtitle: Text(credential.type), trailing: Text( credential.verified ? l10n.accountVerified : l10n.accountUnverified, - style: const TextStyle(color: AppColors.muted, fontSize: 11), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ), _summary(l10n.accountConsentRecords, '${data.consentCount}'), @@ -144,10 +157,10 @@ class _AccountExportPageState extends State { ); Widget _card({required List children}) => Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(AppDimens.cardPadding), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.outlineVariantDark), ), child: Column( @@ -163,7 +176,10 @@ class _AccountExportPageState extends State { Expanded( child: Text( label, - style: const TextStyle(color: AppColors.muted, fontSize: 12), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ), Text(value, style: const TextStyle(fontWeight: FontWeight.w700)), diff --git a/lib/pages/account/account_profile_page.dart b/lib/pages/account/account_profile_page.dart index df3be83..d3aeac9 100644 --- a/lib/pages/account/account_profile_page.dart +++ b/lib/pages/account/account_profile_page.dart @@ -11,6 +11,7 @@ import '../../core/data/services/auth_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; 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/empty_state.dart'; @@ -79,12 +80,15 @@ class _AccountProfilePageState extends State { foregroundColor: AppColors.onPrimary, title: Text(l10n.accountProfile), ), - body: signedIn - ? ListenableBuilder( - listenable: _logic, - builder: (context, _) => _body(l10n), - ) - : const AuthGatePanel(), + body: SafeArea( + top: false, + child: signedIn + ? ListenableBuilder( + listenable: _logic, + builder: (context, _) => _body(l10n), + ) + : const AuthGatePanel(), + ), ), ); } @@ -139,14 +143,17 @@ class _AccountProfilePageState extends State { Text( profile.nickname, style: const TextStyle( - fontSize: 20, + fontSize: AppDimens.pageTitle, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text( '${l10n.accountUid}: ${profile.uid}', - style: const TextStyle(color: AppColors.muted, fontSize: 12), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ], ), @@ -211,7 +218,10 @@ class _AccountProfilePageState extends State { const SizedBox(height: 6), Text( l10n.accountBirthdayHint, - style: const TextStyle(color: AppColors.muted, fontSize: 12), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ], ); @@ -222,7 +232,7 @@ class _AccountProfilePageState extends State { errorText(l10n, _logic.error, l10n.accountProfileSaveFailed), style: TextStyle( color: Theme.of(context).colorScheme.error, - fontSize: 12, + fontSize: AppDimens.caption, ), ), ); @@ -250,7 +260,10 @@ class _AccountProfilePageState extends State { children: [ Text( l10n.accountReadOnlyInfo, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), + style: const TextStyle( + fontSize: AppDimens.listTitle, + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: 10), _info(l10n.accountMaskedAccount, profile.maskedTarget), @@ -283,14 +296,17 @@ class _AccountProfilePageState extends State { Expanded( child: Text( label, - style: const TextStyle(color: AppColors.muted, fontSize: 12), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ), Flexible( child: Text( value.isEmpty ? '-' : value, textAlign: TextAlign.right, - style: const TextStyle(fontSize: 13), + style: const TextStyle(fontSize: AppDimens.body), ), ), ], @@ -337,10 +353,10 @@ class _AccountCard extends StatelessWidget { @override Widget build(BuildContext context) => Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(AppDimens.cardPadding), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.outlineVariantDark), ), child: child, diff --git a/lib/pages/auth/auth_view.dart b/lib/pages/auth/auth_view.dart index 58b886d..b645226 100644 --- a/lib/pages/auth/auth_view.dart +++ b/lib/pages/auth/auth_view.dart @@ -9,6 +9,7 @@ import '../../core/data/remote/api_client.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/platform/haptics.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../l10n/app_localizations.dart'; import 'auth_logic.dart'; @@ -19,8 +20,9 @@ part 'auth_form_widgets.dart'; const double _minTapTarget = 44; const double _brandMarkSize = 48; const double _contentMaxWidth = 440; -const double _cardRadius = 20; -const double _cardPadding = 20; +// Auth keeps its approved 20px card silhouette. +const double _cardRadius = AppDimens.authCardRadius; +const double _cardPadding = AppDimens.cardPadding; const double _cardBorderWidth = 1; const double _fieldHeight = 52; const double _fieldRadius = 14; @@ -163,9 +165,9 @@ class _AuthPageState extends State { keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: EdgeInsets.fromLTRB( - 20, + AppDimens.pageHorizontal, 12, - 20, + AppDimens.pageHorizontal, 28 + MediaQuery.viewInsetsOf(context).bottom, ), children: [ diff --git a/lib/pages/auth/auth_widgets.dart b/lib/pages/auth/auth_widgets.dart index ea66e4f..e4d7543 100644 --- a/lib/pages/auth/auth_widgets.dart +++ b/lib/pages/auth/auth_widgets.dart @@ -103,7 +103,7 @@ class _AuthCard extends StatelessWidget { l10n.authSubtitle, style: const TextStyle( color: AppColors.muted, - fontSize: 13, + fontSize: AppDimens.body, height: 1.45, ), ), diff --git a/lib/pages/discover/discover_view.dart b/lib/pages/discover/discover_view.dart index 7e47800..cc259a2 100644 --- a/lib/pages/discover/discover_view.dart +++ b/lib/pages/discover/discover_view.dart @@ -10,7 +10,9 @@ import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/services/catalog_controller.dart'; import '../../core/data/services/category_controller.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; +import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/remote_image.dart'; import '../../l10n/app_localizations.dart'; @@ -261,7 +263,7 @@ class _DiscoverPageState extends State { ]; return SizedBox( key: const Key('home-category-chips'), - height: 36, + height: AppDimens.tapTarget, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: choices.length, @@ -269,24 +271,31 @@ class _DiscoverPageState extends State { itemBuilder: (context, index) { final choice = choices[index]; final selected = categories.selectedCode == choice.code; - return GestureDetector( - onTap: () => categories.selectCode(choice.code), - child: AnimatedContainer( - duration: const Duration(milliseconds: 180), - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 14), - decoration: BoxDecoration( - color: selected ? AppColors.blue : AppColors.raised, - borderRadius: BorderRadius.circular(18), - ), - child: Text( - choice.label, - maxLines: 1, - style: TextStyle( - color: selected ? AppColors.onPrimary : AppColors.cream, - fontSize: 13, - fontWeight: selected ? FontWeight.w700 : FontWeight.w500, - height: 1.1, + return Semantics( + selected: selected, + button: true, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => categories.selectCode(choice.code), + child: Center( + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: selected ? AppColors.blue : AppColors.raised, + borderRadius: BorderRadius.circular(AppDimens.pillRadius), + ), + child: Text( + choice.label, + maxLines: 1, + style: TextStyle( + color: selected ? AppColors.onPrimary : AppColors.cream, + fontSize: AppDimens.body, + fontWeight: selected ? FontWeight.w700 : FontWeight.w500, + height: 1.1, + ), + ), ), ), ), @@ -296,83 +305,41 @@ class _DiscoverPageState extends State { ); } - Widget _bottomNav(AppLocalizations l10n) => Container( - height: 63, - decoration: const BoxDecoration( - color: AppColors.bar, - border: Border(top: BorderSide(color: AppColors.outlineVariantDark)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _DiscoverNavItem( - icon: Icons.play_circle_outline_rounded, - label: l10n.homeTab, - onTap: () => logic.act( - context, - DiscoverPageAction.home, - embedded: widget.embedded, - onAction: widget.onAction, - ), + Widget _bottomNav(AppLocalizations l10n) => AppBottomNav( + items: [ + AppBottomNavItem( + tabKey: const Key('discover-nav-home'), + icon: Icons.play_circle_outline_rounded, + selectedIcon: Icons.play_circle_fill_rounded, + label: l10n.homeTab, + onTap: () => logic.act( + context, + DiscoverPageAction.home, + embedded: widget.embedded, + onAction: widget.onAction, ), - _DiscoverNavItem( - icon: Icons.explore_outlined, - label: l10n.discoverTab, - selected: true, - onTap: () {}, - ), - _DiscoverNavItem( - icon: Icons.video_library_outlined, - label: l10n.myTab, - onTap: () => logic.act( - context, - DiscoverPageAction.my, - embedded: widget.embedded, - onAction: widget.onAction, - ), - ), - ], - ), - ); -} - -class _DiscoverNavItem extends StatelessWidget { - const _DiscoverNavItem({ - required this.icon, - required this.label, - required this.onTap, - this.selected = false, - }); - - final IconData icon; - final String label; - final VoidCallback onTap; - final bool selected; - - @override - Widget build(BuildContext context) => InkWell( - onTap: onTap, - child: SizedBox( - width: 72, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - size: 21, - color: selected ? AppColors.blueSoft : AppColors.muted, - ), - Text( - label, - style: TextStyle( - color: selected ? AppColors.blueSoft : AppColors.muted, - fontSize: 10, - fontWeight: FontWeight.w700, - ), - ), - ], ), - ), + AppBottomNavItem( + tabKey: const Key('discover-nav-current'), + icon: Icons.explore_outlined, + selectedIcon: Icons.explore_rounded, + label: l10n.discoverTab, + selected: true, + onTap: () {}, + ), + AppBottomNavItem( + tabKey: const Key('discover-nav-my'), + icon: Icons.video_library_outlined, + selectedIcon: Icons.video_library_rounded, + label: l10n.myTab, + onTap: () => logic.act( + context, + DiscoverPageAction.my, + embedded: widget.embedded, + onAction: widget.onAction, + ), + ), + ], ); } @@ -421,24 +388,27 @@ class _HeaderPill extends StatelessWidget { child: InkWell( customBorder: const StadiumBorder(), onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 15, color: _coin), - const SizedBox(width: 4), - Text( - label, - maxLines: 1, - style: const TextStyle( - color: AppColors.cream, - fontSize: 12, - fontWeight: FontWeight.w700, - height: 1.1, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: AppDimens.tapTarget), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 15, color: _coin), + const SizedBox(width: 4), + Text( + label, + maxLines: 1, + style: const TextStyle( + color: AppColors.cream, + fontSize: AppDimens.caption, + fontWeight: FontWeight.w700, + height: 1.1, + ), ), - ), - ], + ], + ), ), ), ), @@ -459,11 +429,13 @@ class _LoginButton extends StatelessWidget { style: TextButton.styleFrom( foregroundColor: Colors.white, backgroundColor: AppColors.blue, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: const EdgeInsets.symmetric(horizontal: 14), + minimumSize: const Size(AppDimens.tapTarget, AppDimens.tapTarget), shape: const StadiumBorder(), - textStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), + textStyle: const TextStyle( + fontSize: AppDimens.body, + fontWeight: FontWeight.w600, + ), ), child: Text(label, maxLines: 1), ); diff --git a/lib/pages/discover/widgets/home_drama_grid.dart b/lib/pages/discover/widgets/home_drama_grid.dart index 50aabe8..3ecf9a0 100644 --- a/lib/pages/discover/widgets/home_drama_grid.dart +++ b/lib/pages/discover/widgets/home_drama_grid.dart @@ -59,7 +59,7 @@ class _HomeDramaGrid extends StatelessWidget { Widget _dramaGrid(List dramas) => SliverPadding( key: const Key('home-all-dramas-section'), - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: AppDimens.pageHorizontal), sliver: SliverGrid( key: const Key('home-all-dramas-grid'), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -101,12 +101,12 @@ class _HomeDramaCard extends StatelessWidget { return InkWell( key: Key('home-drama-card-${drama.id}'), onTap: onTap, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), child: AspectRatio( key: Key('home-drama-cover-${drama.id}'), aspectRatio: 2 / 3, child: ClipRRect( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), child: Stack( fit: StackFit.expand, children: [ @@ -143,7 +143,7 @@ class _HomeDramaCard extends StatelessWidget { l10n.hot, style: const TextStyle( color: AppColors.onPrimary, - fontSize: 10, + fontSize: AppDimens.caption, fontWeight: FontWeight.w800, height: 1.1, ), @@ -166,7 +166,7 @@ class _HomeDramaCard extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.cream, - fontSize: 13, + fontSize: AppDimens.body, fontWeight: FontWeight.w800, height: 1.15, ), @@ -178,8 +178,8 @@ class _HomeDramaCard extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: Color(0xE6FFFFFF), - fontSize: 11, + color: AppColors.cream, + fontSize: AppDimens.caption, fontWeight: FontWeight.w400, height: 1.2, ), diff --git a/lib/pages/downloads/downloads_page.dart b/lib/pages/downloads/downloads_page.dart index 8d16604..ba7aff8 100644 --- a/lib/pages/downloads/downloads_page.dart +++ b/lib/pages/downloads/downloads_page.dart @@ -13,6 +13,7 @@ import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; import '../../core/platform/haptics.dart'; 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/empty_state.dart'; @@ -60,9 +61,12 @@ class _DownloadsPageState extends State { foregroundColor: Colors.white, title: Text(l10n.downloads), ), - body: signedIn - ? _DownloadsBody(downloads: context.watch()) - : const AuthGatePanel(), + body: SafeArea( + top: false, + child: signedIn + ? _DownloadsBody(downloads: context.watch()) + : const AuthGatePanel(), + ), ), ); } @@ -194,11 +198,11 @@ class _PolicyPanel extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return Container( key: const Key('download-policy'), - padding: const EdgeInsets.fromLTRB(14, 14, 14, 10), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFF2A2C33)), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), + border: Border.all(color: AppColors.outlineVariantDark), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -219,15 +223,21 @@ class _PolicyPanel extends StatelessWidget { const SizedBox(height: 6), Text( policy.expirePolicyText, - style: const TextStyle(color: Color(0xFFB7B3B8), fontSize: 12), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), - const Divider(height: 22, color: Color(0xFF2A2C33)), + const Divider(height: 22, color: AppColors.outlineVariantDark), Row( children: [ Expanded( child: Text( l10n.downloadWifiOnly, - style: const TextStyle(color: Colors.white, fontSize: 13), + style: const TextStyle( + color: Colors.white, + fontSize: AppDimens.body, + ), ), ), Switch( @@ -242,7 +252,10 @@ class _PolicyPanel extends StatelessWidget { Expanded( child: Text( l10n.downloadQuality, - style: const TextStyle(color: Colors.white, fontSize: 13), + style: const TextStyle( + color: Colors.white, + fontSize: AppDimens.body, + ), ), ), DropdownButton( @@ -302,7 +315,7 @@ class _LicenseTile extends StatelessWidget { padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -355,10 +368,12 @@ class _LicenseTile extends StatelessWidget { ), const SizedBox(height: 8), ClipRRect( - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.circular(AppDimens.progressRadius), child: LinearProgressIndicator( - minHeight: 4, + minHeight: AppDimens.progressHeight, value: (license.progressPercent / 100).clamp(0.0, 1.0), + backgroundColor: Colors.white24, + valueColor: const AlwaysStoppedAnimation(AppColors.blueSoft), ), ), if (license.expireAt != null) ...[ @@ -367,7 +382,10 @@ class _LicenseTile extends StatelessWidget { l10n.downloadExpiresAt( formatLocalizedDateTime(context, license.expireAt!), ), - style: const TextStyle(color: Color(0xFF8D8990), fontSize: 11), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ], ], diff --git a/lib/pages/drama_detail/drama_detail_view.dart b/lib/pages/drama_detail/drama_detail_view.dart index 05700c9..e3b25d2 100644 --- a/lib/pages/drama_detail/drama_detail_view.dart +++ b/lib/pages/drama_detail/drama_detail_view.dart @@ -13,6 +13,7 @@ import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_content.dart'; import '../../core/platform/haptics.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 '../../core/widgets/auth_gate.dart'; @@ -78,14 +79,24 @@ class _DramaDetailPageState extends State { Widget _page(BuildContext context) { final l10n = AppLocalizations.of(context)!; if (logic.loading) { - return const Scaffold( + return Scaffold( backgroundColor: AppColors.ink, - body: Center(child: CircularProgressIndicator()), + appBar: AppBar( + backgroundColor: AppColors.ink, + foregroundColor: AppColors.onPrimary, + title: Text(localizedDramaTitle(l10n, widget.drama)), + ), + body: const Center(child: CircularProgressIndicator()), ); } if (logic.failed) { return Scaffold( backgroundColor: AppColors.ink, + appBar: AppBar( + backgroundColor: AppColors.ink, + foregroundColor: AppColors.onPrimary, + title: Text(localizedDramaTitle(l10n, widget.drama)), + ), body: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), @@ -140,11 +151,25 @@ class _DramaDetailPageState extends State { _favoriteAction(context, l10n), ], flexibleSpace: FlexibleSpaceBar( - background: RemoteImage(drama.posterUrl), + background: Stack( + fit: StackFit.expand, + children: [ + RemoteImage(drama.posterUrl), + const DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.center, + colors: [Color(0x99000000), Color(0x00000000)], + ), + ), + ), + ], + ), ), ), SliverPadding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 40), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 40), sliver: SliverList( delegate: SliverChildListDelegate([ Text( @@ -158,16 +183,17 @@ class _DramaDetailPageState extends State { const SizedBox(height: 8), Text( localizedDramaTitle(l10n, drama), - style: Theme.of(context).textTheme.headlineLarge?.copyWith( + style: const TextStyle( + fontSize: AppDimens.detailHeadline, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 10), Text( localizedDramaSynopsis(l10n, drama), - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: Theme.of( + context, + ).textTheme.bodyLarge?.copyWith(color: AppColors.muted), ), const SizedBox(height: 20), Column( @@ -222,15 +248,16 @@ class _DramaDetailPageState extends State { const SizedBox(height: 30), Text( l10n.episodes, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontSize: AppDimens.sectionTitle, + fontWeight: FontWeight.w700, + ), ), ]), ), ), SliverPadding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 40), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 40), sliver: SliverList.builder( itemCount: drama.episodes.length, itemBuilder: (context, index) { @@ -364,10 +391,10 @@ class _ResumeBar extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( - borderRadius: BorderRadius.circular(2), + borderRadius: BorderRadius.circular(AppDimens.progressRadius), child: LinearProgressIndicator( value: fraction, - minHeight: 3, + minHeight: AppDimens.progressHeight, backgroundColor: Colors.white24, valueColor: const AlwaysStoppedAnimation(AppColors.blueSoft), ), @@ -376,7 +403,10 @@ class _ResumeBar extends StatelessWidget { const SizedBox(height: 4), Text( label!, - style: const TextStyle(color: Colors.white54, fontSize: 11), + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, + ), ), ], ], diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index 1ebf464..d1626fb 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -59,9 +59,12 @@ class _FavoritesPageState extends State { foregroundColor: Colors.white, title: Text(l10n.myFavorites), ), - body: signedIn - ? _FavoritesBody(favorites: context.watch()) - : const AuthGatePanel(), + body: SafeArea( + top: false, + child: signedIn + ? _FavoritesBody(favorites: context.watch()) + : const AuthGatePanel(), + ), ), ); } @@ -114,7 +117,7 @@ class _FavoritesBody extends StatelessWidget { onPressed: () => _remove(context, drama.id), icon: const Icon( Icons.favorite_rounded, - color: Color(0xFFD74A7D), + color: AppColors.favorite, ), ), ); diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 4a309c2..32297ea 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -13,6 +13,7 @@ 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/auth_gate.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/server_endpoint_dialog.dart'; @@ -239,57 +240,42 @@ class _HomeTabsState extends State<_HomeTabs> { ), bottomNavigationBar: logic.videoImmersive ? null - : SafeArea( - top: false, - minimum: const EdgeInsets.only(bottom: 2), - child: Container( - key: const Key('home-tabs-bar'), - height: 64, - decoration: BoxDecoration( - color: AppColors.bar, - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.outlineVariant, - ), - ), + : AppBottomNav( + barKey: const Key('home-tabs-bar'), + items: [ + AppBottomNavItem( + tabKey: const Key('tab-discover'), + icon: Icons.home_outlined, + selectedIcon: Icons.home_rounded, + label: l10n.homeTab, + selected: logic.tab == 1, + onTap: () => _selectTab(context, 1), ), - child: Row( - children: [ - _TabButton( - key: const Key('tab-discover'), - icon: Icons.home_outlined, - selectedIcon: Icons.home_rounded, - label: l10n.homeTab, - selected: logic.tab == 1, - onTap: () => _selectTab(context, 1), - ), - _TabButton( - key: const Key('tab-home'), - icon: Icons.movie_outlined, - selectedIcon: Icons.movie_rounded, - label: l10n.shortsTab, - selected: logic.tab == 0, - onTap: () => _selectTab(context, 0), - ), - _TabButton( - key: const Key('tab-my-list'), - icon: Icons.bookmark_border_rounded, - selectedIcon: Icons.bookmark_rounded, - label: l10n.myListTab, - selected: logic.tab == 2 && !logic.profile, - onTap: () => _selectTab(context, 2), - ), - _TabButton( - key: const Key('tab-my'), - icon: Icons.person_outline_rounded, - selectedIcon: Icons.person_rounded, - label: l10n.myTab, - selected: logic.tab == 2 && logic.profile, - onTap: () => _selectTab(context, 2, myProfile: true), - ), - ], + AppBottomNavItem( + tabKey: const Key('tab-home'), + icon: Icons.movie_outlined, + selectedIcon: Icons.movie_rounded, + label: l10n.shortsTab, + selected: logic.tab == 0, + onTap: () => _selectTab(context, 0), ), - ), + AppBottomNavItem( + tabKey: const Key('tab-my-list'), + icon: Icons.bookmark_border_rounded, + selectedIcon: Icons.bookmark_rounded, + label: l10n.myListTab, + selected: logic.tab == 2 && !logic.profile, + onTap: () => _selectTab(context, 2), + ), + AppBottomNavItem( + tabKey: const Key('tab-my'), + icon: Icons.person_outline_rounded, + selectedIcon: Icons.person_rounded, + label: l10n.myTab, + selected: logic.tab == 2 && logic.profile, + onTap: () => _selectTab(context, 2, myProfile: true), + ), + ], ), ), ); @@ -309,67 +295,3 @@ class _HomeTabsState extends State<_HomeTabs> { } } } - -class _TabButton extends StatelessWidget { - const _TabButton({ - super.key, - required this.icon, - required this.selectedIcon, - required this.label, - required this.selected, - required this.onTap, - }); - final IconData icon; - final IconData selectedIcon; - final String label; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) => Expanded( - child: Semantics( - button: true, - selected: selected, - label: label, - child: InkWell( - onTap: onTap, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 48), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 160), - curve: Curves.easeOut, - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 3, - ), - decoration: BoxDecoration( - color: selected ? AppColors.blue : null, - borderRadius: BorderRadius.circular(14), - ), - child: Icon( - selected ? selectedIcon : icon, - size: 22, - color: selected ? AppColors.onPrimary : AppColors.muted, - ), - ), - const SizedBox(height: 2), - Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, - fontWeight: selected ? FontWeight.w700 : FontWeight.w500, - color: selected ? AppColors.blueSoft : AppColors.muted, - ), - ), - ], - ), - ), - ), - ), - ); -} diff --git a/lib/pages/home/widgets/home_quick_drawer.dart b/lib/pages/home/widgets/home_quick_drawer.dart index d419115..b18575f 100644 --- a/lib/pages/home/widgets/home_quick_drawer.dart +++ b/lib/pages/home/widgets/home_quick_drawer.dart @@ -7,6 +7,7 @@ import '../../../core/config/locale_controller.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/formatting/localized_content.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/theme/app_dimens.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; @@ -51,7 +52,7 @@ class HomeQuickDrawer extends StatelessWidget { children: [ const CircleAvatar( radius: 30, - backgroundColor: Color(0xFF4B4142), + backgroundColor: AppColors.raised, child: Icon( Icons.person_rounded, size: 34, @@ -74,7 +75,7 @@ class HomeQuickDrawer extends StatelessWidget { const SizedBox(height: 3), Text( l10n.localDemoProfile, - style: const TextStyle(color: Color(0xFFE7E2E0)), + style: const TextStyle(color: AppColors.cream), ), ], ), @@ -91,7 +92,9 @@ class HomeQuickDrawer extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular( + AppDimens.innerRadius, + ), child: RemoteImage( drama.posterUrl, width: 74, @@ -115,7 +118,7 @@ class HomeQuickDrawer extends StatelessWidget { const SizedBox(height: 7), Text( l10n.episodeCount(drama.episodeCount), - style: const TextStyle(color: Color(0xFFE7E2E0)), + style: const TextStyle(color: AppColors.cream), ), ], ), @@ -229,7 +232,7 @@ class _SectionCard extends StatelessWidget { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: HomeQuickDrawer._card, - borderRadius: BorderRadius.circular(18), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -259,11 +262,11 @@ class _QuickAction extends StatelessWidget { @override Widget build(BuildContext context) => Material( - color: const Color(0xFF3B3B3F), - borderRadius: BorderRadius.circular(12), + color: AppColors.raised, + borderRadius: BorderRadius.circular(AppDimens.innerRadius), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), child: Padding( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 7), child: Column( diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart index e4d794b..6acf5ac 100644 --- a/lib/pages/mine/mine_view.dart +++ b/lib/pages/mine/mine_view.dart @@ -14,8 +14,10 @@ import '../../core/data/services/interaction_controller.dart'; import '../../core/data/services/favorite_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/widgets/empty_state.dart'; +import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/remote_image.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../l10n/app_localizations.dart'; import 'mine_binding.dart'; @@ -139,9 +141,9 @@ class _MyPageState extends State { Center( child: Text( l10n.brandMark, - style: TextStyle( - color: Color(0xFFD2CDD0), - fontSize: 10, + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.caption, letterSpacing: 1.3, ), ), @@ -173,7 +175,10 @@ class _MyPageState extends State { const SizedBox(width: 7), Text( l10n.myListTab, - style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w800), + style: const TextStyle( + fontSize: AppDimens.pageTitle, + fontWeight: FontWeight.w800, + ), ), const Spacer(), IconButton( @@ -212,7 +217,7 @@ class _MyPageState extends State { l10n.preferencesHeading, style: const TextStyle( color: AppColors.muted, - fontSize: 12, + fontSize: AppDimens.caption, fontWeight: FontWeight.w800, letterSpacing: 1.1, ), @@ -316,10 +321,10 @@ class _MyPageState extends State { Widget _wallet(AppLocalizations l10n) { return Container( - padding: const EdgeInsets.all(14), + padding: const EdgeInsets.all(AppDimens.cardPadding), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -374,7 +379,7 @@ class _MyPageState extends State { padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.ink, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: color.withValues(alpha: 0.45)), ), child: Column( @@ -396,7 +401,6 @@ class _MyPageState extends State { foregroundColor: color == AppColors.blue ? Colors.white : AppColors.onGold, - visualDensity: VisualDensity.compact, shape: const StadiumBorder(), ), child: Text(action, maxLines: 1, overflow: TextOverflow.ellipsis), @@ -417,6 +421,7 @@ class _MyPageState extends State { ListTile( dense: true, visualDensity: VisualDensity.compact, + minTileHeight: AppDimens.tapTarget, leading: const Icon( Icons.card_giftcard_rounded, color: AppColors.blueSoft, @@ -432,6 +437,7 @@ class _MyPageState extends State { ListTile( dense: true, visualDensity: VisualDensity.compact, + minTileHeight: AppDimens.tapTarget, key: const Key('mine-notices'), leading: const Icon( Icons.notifications_none_rounded, @@ -447,6 +453,7 @@ class _MyPageState extends State { ListTile( dense: true, visualDensity: VisualDensity.compact, + minTileHeight: AppDimens.tapTarget, leading: const Icon( Icons.history_rounded, color: AppColors.blueSoft, @@ -461,6 +468,7 @@ class _MyPageState extends State { ListTile( dense: true, visualDensity: VisualDensity.compact, + minTileHeight: AppDimens.tapTarget, leading: const Icon( Icons.favorite_outline_rounded, color: AppColors.blueSoft, @@ -475,6 +483,7 @@ class _MyPageState extends State { ListTile( dense: true, visualDensity: VisualDensity.compact, + minTileHeight: AppDimens.tapTarget, leading: const Icon( Icons.download_outlined, color: AppColors.blueSoft, @@ -489,6 +498,7 @@ class _MyPageState extends State { ListTile( dense: true, visualDensity: VisualDensity.compact, + minTileHeight: AppDimens.tapTarget, leading: const Icon( Icons.settings_rounded, color: AppColors.blueSoft, @@ -534,30 +544,36 @@ class _MyPageState extends State { child: Row( children: [ for (var index = 0; index < tabs.length; index++) ...[ - InkWell( - onTap: () { - logic.selectSection(index); - // 打开收藏页签时以服务端为准刷新一次(多设备/多入口都能对齐) - if (index == 1) unawaited(favorites.load()); - }, - borderRadius: BorderRadius.circular(20), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - decoration: BoxDecoration( - color: logic.section == index - ? AppColors.blue - : AppColors.raised, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - tabs[index], - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w700, + Semantics( + selected: logic.section == index, + button: true, + child: InkWell( + onTap: () { + logic.selectSection(index); + // 打开收藏页签时以服务端为准刷新一次(多设备/多入口都能对齐) + if (index == 1) unawaited(favorites.load()); + }, + borderRadius: BorderRadius.circular(AppDimens.pillRadius), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + decoration: BoxDecoration( + color: logic.section == index + ? AppColors.blue + : AppColors.raised, + borderRadius: BorderRadius.circular( + AppDimens.pillRadius, + ), + ), + child: Text( + tabs[index], + style: const TextStyle( + color: Colors.white, + fontSize: AppDimens.body, + fontWeight: FontWeight.w700, + ), ), ), ), @@ -581,14 +597,14 @@ class _MyPageState extends State { selected: logic.filter == index, onSelected: (_) => logic.selectFilter(index), showCheckmark: false, - visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.padded, backgroundColor: AppColors.raised, selectedColor: AppColors.blue, labelStyle: TextStyle( color: logic.filter == index ? Colors.white - : const Color(0xFFD5CED0), - fontSize: 11, + : AppColors.muted, + fontSize: AppDimens.caption, fontWeight: FontWeight.w700, ), side: BorderSide.none, @@ -606,12 +622,12 @@ class _MyPageState extends State { embedded: widget.embedded, onAction: widget.onAction, ), - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), child: Container( - padding: const EdgeInsets.all(9), + padding: const EdgeInsets.all(AppDimens.innerPadding), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), ), child: Row( children: [ @@ -626,7 +642,7 @@ class _MyPageState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 15, + fontSize: AppDimens.listTitle, fontWeight: FontWeight.w700, ), ), @@ -634,8 +650,8 @@ class _MyPageState extends State { Text( l10n.dramaTag, style: const TextStyle( - color: Color(0xFFE6E1E3), - fontSize: 12, + color: AppColors.cream, + fontSize: AppDimens.caption, ), ), const SizedBox(height: 32), @@ -645,7 +661,7 @@ class _MyPageState extends State { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.blueSoft, - fontSize: 10, + fontSize: AppDimens.caption, ), ), ], @@ -703,7 +719,7 @@ class _MyPageState extends State { l10n.history, style: const TextStyle( color: Colors.white, - fontSize: 14, + fontSize: AppDimens.body, fontWeight: FontWeight.w700, ), ), @@ -714,7 +730,7 @@ class _MyPageState extends State { Expanded( child: Text( l10n.progressError, - style: const TextStyle(color: Color(0xFFE6E1E3)), + style: const TextStyle(color: AppColors.cream), ), ), TextButton( @@ -740,7 +756,7 @@ class _MyPageState extends State { Expanded( child: Text( errorText(l10n, logic.recentError, l10n.progressError), - style: const TextStyle(color: Color(0xFFE6E1E3)), + style: const TextStyle(color: AppColors.cream), ), ), TextButton( @@ -794,17 +810,17 @@ class _MyPageState extends State { ); } }, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), child: Container( - padding: const EdgeInsets.all(9), + padding: const EdgeInsets.all(AppDimens.innerPadding), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), ), child: Row( children: [ ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), child: RemoteImage(drama.posterUrl, width: 74, height: 103), ), const SizedBox(width: 11), @@ -817,7 +833,7 @@ class _MyPageState extends State { maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 15, + fontSize: AppDimens.listTitle, fontWeight: FontWeight.w700, ), ), @@ -825,56 +841,54 @@ class _MyPageState extends State { Text( l10n.episodeCount(drama.episodeCount), style: const TextStyle( - color: Color(0xFFE6E1E3), - fontSize: 12, + color: AppColors.cream, + fontSize: AppDimens.caption, ), ), ], ), ), - const Icon(Icons.chevron_right_rounded, color: Color(0xFFF2EEEF)), + const Icon(Icons.chevron_right_rounded, color: AppColors.cream), ], ), ), ); - Widget _bottomNav(AppLocalizations l10n) => Container( - height: 63, - decoration: const BoxDecoration( - color: AppColors.bar, - border: Border(top: BorderSide(color: AppColors.outlineVariantDark)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _NavItem( - icon: Icons.play_circle_outline_rounded, - label: l10n.homeTab, - onTap: () => logic.act( - context, - MyPageAction.home, - embedded: widget.embedded, - onAction: widget.onAction, - ), + Widget _bottomNav(AppLocalizations l10n) => AppBottomNav( + items: [ + AppBottomNavItem( + tabKey: const Key('mine-nav-home'), + icon: Icons.play_circle_outline_rounded, + selectedIcon: Icons.play_circle_fill_rounded, + label: l10n.homeTab, + onTap: () => logic.act( + context, + MyPageAction.home, + embedded: widget.embedded, + onAction: widget.onAction, ), - _NavItem( - icon: Icons.explore_outlined, - label: l10n.discoverTab, - onTap: () => logic.act( - context, - MyPageAction.discover, - embedded: widget.embedded, - onAction: widget.onAction, - ), + ), + AppBottomNavItem( + tabKey: const Key('mine-nav-discover'), + icon: Icons.explore_outlined, + selectedIcon: Icons.explore_rounded, + label: l10n.discoverTab, + onTap: () => logic.act( + context, + MyPageAction.discover, + embedded: widget.embedded, + onAction: widget.onAction, ), - _NavItem( - icon: Icons.video_library_outlined, - label: l10n.myTab, - selected: true, - onTap: () {}, - ), - ], - ), + ), + AppBottomNavItem( + tabKey: const Key('mine-nav-current'), + icon: Icons.video_library_outlined, + selectedIcon: Icons.video_library_rounded, + label: l10n.myTab, + selected: true, + onTap: () {}, + ), + ], ); Widget _poster({required double width, required double height}) => ClipRRect( @@ -883,42 +897,6 @@ class _MyPageState extends State { ); } -class _NavItem extends StatelessWidget { - const _NavItem({ - required this.icon, - required this.label, - required this.onTap, - this.selected = false, - }); - - final IconData icon; - final String label; - final VoidCallback onTap; - final bool selected; - - @override - Widget build(BuildContext context) => InkWell( - onTap: onTap, - child: SizedBox( - width: 72, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: 21, color: selected ? _accent : AppColors.muted), - Text( - label, - style: TextStyle( - color: selected ? _accent : AppColors.muted, - fontSize: 10, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), - ); -} - String _clock(Duration duration) { final seconds = duration.inSeconds; return '${(seconds ~/ 60).toString().padLeft(2, '0')}:${(seconds % 60).toString().padLeft(2, '0')}'; diff --git a/lib/pages/notices/notices_page.dart b/lib/pages/notices/notices_page.dart index c828adf..0b16bac 100644 --- a/lib/pages/notices/notices_page.dart +++ b/lib/pages/notices/notices_page.dart @@ -11,6 +11,7 @@ import '../../core/data/services/notices_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; 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 '../../l10n/app_localizations.dart'; @@ -52,9 +53,12 @@ class _NoticesPageState extends State { foregroundColor: AppColors.onPrimary, title: Text(l10n.notices), ), - body: ListenableBuilder( - listenable: _controller, - builder: (context, _) => _body(l10n), + body: SafeArea( + top: false, + child: ListenableBuilder( + listenable: _controller, + builder: (context, _) => _body(l10n), + ), ), ), ); @@ -214,12 +218,12 @@ class _NoticeTile extends StatelessWidget { return Material( key: ValueKey('notice-item-${notice.id}'), color: AppColors.card, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Padding( - padding: const EdgeInsets.all(14), + padding: const EdgeInsets.all(AppDimens.innerPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -233,7 +237,9 @@ class _NoticeTile extends StatelessWidget { ), decoration: BoxDecoration( color: AppColors.bluePressed, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular( + AppDimens.pillRadius, + ), ), child: Text( type, @@ -241,7 +247,7 @@ class _NoticeTile extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onPrimary, - fontSize: 11, + fontSize: AppDimens.caption, ), ), ), @@ -257,7 +263,7 @@ class _NoticeTile extends StatelessWidget { textAlign: TextAlign.right, style: const TextStyle( color: AppColors.muted, - fontSize: 11, + fontSize: AppDimens.caption, ), ), ), @@ -271,7 +277,7 @@ class _NoticeTile extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onPrimary, - fontSize: 16, + fontSize: AppDimens.sectionTitle, fontWeight: FontWeight.w700, ), ), @@ -283,7 +289,7 @@ class _NoticeTile extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.muted, - fontSize: 13, + fontSize: AppDimens.body, height: 1.35, ), ), diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 9b142b2..94f744a 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -18,6 +18,7 @@ import '../../core/formatting/localized_content.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/auth_gate.dart'; import '../../core/widgets/download_action.dart'; import '../../l10n/app_localizations.dart'; @@ -734,14 +735,14 @@ class _PlaybackOverlay extends StatelessWidget { children: [ _RailAction( key: const Key('rail-like'), - icon: Icons.favorite_border, + icon: liked ? Icons.favorite : Icons.favorite_border, label: l10n.like, selected: liked, onTap: onLike, ), _RailAction( key: const Key('rail-save'), - icon: Icons.bookmark_border, + icon: saved ? Icons.bookmark : Icons.bookmark_border, label: l10n.save, selected: saved, onTap: onSave, @@ -787,30 +788,31 @@ class _PlaybackOverlay extends StatelessWidget { ], ); - Widget _fallbackNav() => Container( - height: 62, - color: const Color(0xF20C0C0F), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _BottomAction( - icon: Icons.play_circle_outline_rounded, - label: l10n.homeTab, - selected: true, - onTap: () {}, - ), - _BottomAction( - icon: Icons.explore_outlined, - label: l10n.discoverTab, - onTap: onDiscover, - ), - _BottomAction( - icon: Icons.video_library_outlined, - label: l10n.myTab, - onTap: onMy, - ), - ], - ), + Widget _fallbackNav() => AppBottomNav( + items: [ + AppBottomNavItem( + tabKey: const Key('player-nav-home'), + icon: Icons.play_circle_outline_rounded, + selectedIcon: Icons.play_circle_fill_rounded, + label: l10n.homeTab, + selected: true, + onTap: () {}, + ), + AppBottomNavItem( + tabKey: const Key('player-nav-discover'), + icon: Icons.explore_outlined, + selectedIcon: Icons.explore_rounded, + label: l10n.discoverTab, + onTap: onDiscover, + ), + AppBottomNavItem( + tabKey: const Key('player-nav-my'), + icon: Icons.video_library_outlined, + selectedIcon: Icons.video_library_rounded, + label: l10n.myTab, + onTap: onMy, + ), + ], ); } @@ -844,43 +846,3 @@ class _RailAction extends StatelessWidget { icon: Icon(icon, size: _railIconSize, weight: _railIconWeight, fill: 0), ); } - -class _BottomAction extends StatelessWidget { - const _BottomAction({ - required this.icon, - required this.label, - required this.onTap, - this.selected = false, - }); - - final IconData icon; - final String label; - final VoidCallback onTap; - final bool selected; - - @override - Widget build(BuildContext context) => InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - color: selected ? AppColors.blueSoft : const Color(0xFFF3EEEA), - ), - const SizedBox(height: 2), - Text( - label, - style: TextStyle( - fontSize: 11, - color: selected ? AppColors.blueSoft : const Color(0xFFF3EEEA), - ), - ), - ], - ), - ), - ); -} diff --git a/lib/pages/player/widgets/comment_tile.dart b/lib/pages/player/widgets/comment_tile.dart index 52842b5..56c1c19 100644 --- a/lib/pages/player/widgets/comment_tile.dart +++ b/lib/pages/player/widgets/comment_tile.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import '../../../core/data/models/comment.dart'; import '../../../core/data/services/comment_controller.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/theme/app_dimens.dart'; import '../../../l10n/app_localizations.dart'; const String _deleteAction = 'delete'; @@ -43,7 +44,9 @@ class CommentTile extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final name = comment.nickname.isEmpty ? l10n.anonymousUser : comment.nickname; + final name = comment.nickname.isEmpty + ? l10n.anonymousUser + : comment.nickname; return Padding( key: Key('comment-item-${comment.id}'), padding: const EdgeInsets.symmetric(vertical: 8), @@ -111,7 +114,7 @@ class CommentTile extends StatelessWidget { key: Key('comment-pending-${comment.id}'), style: const TextStyle( color: AppColors.blueSoft, - fontSize: 11, + fontSize: AppDimens.caption, ), ), ), diff --git a/lib/pages/watch_records/watch_records_page.dart b/lib/pages/watch_records/watch_records_page.dart index 4ad5fa8..d5250c5 100644 --- a/lib/pages/watch_records/watch_records_page.dart +++ b/lib/pages/watch_records/watch_records_page.dart @@ -15,6 +15,7 @@ import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; import '../../core/platform/haptics.dart'; 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/empty_state.dart'; @@ -41,7 +42,10 @@ class WatchRecordsPage extends StatelessWidget { foregroundColor: Colors.white, title: Text(l10n.watchHistory), ), - body: signedIn ? const _WatchRecordsBody() : const AuthGatePanel(), + body: SafeArea( + top: false, + child: signedIn ? const _WatchRecordsBody() : const AuthGatePanel(), + ), ), ); } @@ -207,16 +211,19 @@ class _RecordTile extends StatelessWidget { if (drama == null || record.detailLoading) { return Container( key: ValueKey('watch-record-${record.dramaId}'), - height: 112, + height: AppDimens.watchPlaceholderHeight, decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), ), - child: const Center( - child: SizedBox( - width: 22, - height: 22, - child: CircularProgressIndicator(strokeWidth: 2), + child: const Padding( + padding: EdgeInsets.all(AppDimens.innerPadding), + child: Center( + child: SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2), + ), ), ), ); @@ -227,12 +234,12 @@ class _RecordTile extends StatelessWidget { return Material( key: ValueKey('watch-record-${record.dramaId}'), color: AppColors.card, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onOpen, child: Padding( - padding: const EdgeInsets.all(9), + padding: const EdgeInsets.all(AppDimens.innerPadding), child: Row( children: [ ClipRRect( @@ -250,7 +257,7 @@ class _RecordTile extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, - fontSize: 15, + fontSize: AppDimens.listTitle, fontWeight: FontWeight.w700, ), ), @@ -260,15 +267,21 @@ class _RecordTile extends StatelessWidget { key: ValueKey('watch-record-episode-${record.dramaId}'), style: const TextStyle( color: AppColors.muted, - fontSize: 12, + fontSize: AppDimens.caption, ), ), const SizedBox(height: 7), ClipRRect( - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.circular( + AppDimens.progressRadius, + ), child: LinearProgressIndicator( - minHeight: 4, + minHeight: AppDimens.progressHeight, value: record.progress.fraction, + backgroundColor: Colors.white24, + valueColor: const AlwaysStoppedAnimation( + AppColors.blueSoft, + ), ), ), const SizedBox(height: 6), @@ -279,7 +292,7 @@ class _RecordTile extends StatelessWidget { ), style: const TextStyle( color: AppColors.blueSoft, - fontSize: 11, + fontSize: AppDimens.caption, ), ), if (record.progress.updatedAt != null) @@ -292,8 +305,8 @@ class _RecordTile extends StatelessWidget { ), key: ValueKey('watch-record-updated-${record.dramaId}'), style: const TextStyle( - color: Color(0xFF8D8990), - fontSize: 11, + color: AppColors.muted, + fontSize: AppDimens.caption, ), ), ], @@ -324,10 +337,10 @@ class _FailedRecordTile extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return Container( key: ValueKey('watch-record-failed-${record.dramaId}'), - padding: const EdgeInsets.all(14), + padding: const EdgeInsets.all(AppDimens.innerPadding), decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppDimens.innerRadius), ), child: Row( children: [