From df5fbe5b08c25c63612b672bbcffacb0d4078d43 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 19 Oct 2026 10:16:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=A2=9E=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E8=A7=89=20token=20=E4=B8=8E=E7=BB=9F=E4=B8=80=E5=AF=BC?= =?UTF-8?q?=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app.dart | 12 ++- lib/core/theme/app_colors.dart | 3 + lib/core/theme/app_dimens.dart | 40 ++++++++++ lib/core/theme/app_theme.dart | 28 +++++++ lib/core/widgets/app_bottom_nav.dart | 109 +++++++++++++++++++++++++++ test/home/app_bottom_nav_test.dart | 46 +++++++++++ test/theme/app_dimens_test.dart | 23 ++++++ 7 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 lib/core/theme/app_dimens.dart create mode 100644 lib/core/widgets/app_bottom_nav.dart create mode 100644 test/home/app_bottom_nav_test.dart create mode 100644 test/theme/app_dimens_test.dart diff --git a/lib/app.dart b/lib/app.dart index ac1fc0d..ba3c8c7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'core/config/locale_controller.dart'; @@ -39,6 +40,7 @@ import 'core/data/services/playback_session.dart'; import 'core/data/services/video_player_port.dart'; import 'core/platform/haptics.dart'; import 'core/theme/app_theme.dart'; +import 'core/theme/app_colors.dart'; import 'l10n/app_localizations.dart'; import 'pages/home/home_view.dart'; @@ -218,9 +220,17 @@ class _LumaDramaAppState extends State { localeResolutionCallback: (deviceLocale, _) => resolveAppLocale(locale.locale, deviceLocale), debugShowCheckedModeBanner: false, - themeMode: ThemeMode.system, + themeMode: ThemeMode.dark, theme: AppTheme.light(), darkTheme: AppTheme.dark(), + builder: (context, child) => AnnotatedRegion( + value: SystemUiOverlayStyle.light.copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: AppColors.bar, + systemNavigationBarIconBrightness: Brightness.light, + ), + child: child ?? const SizedBox.shrink(), + ), home: const HomePage(), ), ), diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 468c587..1f3e855 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -20,6 +20,9 @@ abstract final class AppColors { /// Coins, rewards, and the playback progress line. static const gold = Color(0xFFF6C453); + /// Favorite/like heart without competing with the chili primary action. + static const favorite = Color(0xFFD74A7D); + /// Text placed on [gold]. static const onGold = Color(0xFF2A1810); diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart new file mode 100644 index 0000000..0da82a6 --- /dev/null +++ b/lib/core/theme/app_dimens.dart @@ -0,0 +1,40 @@ +// Author: fruit.com.cn + +/// K20 cross-page spacing, type, radius, and control sizes. +abstract final class AppDimens { + static const double spaceXs = 4; + static const double spaceSm = 8; + static const double spaceMd = 12; + static const double spaceLg = 16; + static const double spaceXl = 24; + + static const double pageHorizontal = 16; + static const double cardRadius = 16; + static const double cardPadding = 16; + static const double innerRadius = 12; + static const double innerPadding = 12; + static const double pillRadius = 20; + + static const double caption = 12; + static const double body = 14; + static const double listTitle = 15; + static const double sectionTitle = 16; + static const double pageTitle = 20; + static const double detailHeadline = 24; + static const double navLabel = 11; + + static const double buttonHeight = 48; + static const double primaryButtonHeight = 52; + static const double tapTarget = 44; + + static const double navHeight = 64; + static const double navIcon = 22; + static const double progressHeight = 4; + static const double progressRadius = 4; + + static const double accountCardHeight = 64; + static const double accountIcon = 18; + static const double accountLabel = 11; + + static const double watchPlaceholderHeight = 114; +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index f017d21..432bdb1 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,8 +1,10 @@ // Author: fruit.com.cn import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'app_colors.dart'; +import 'app_dimens.dart'; /// Light paper and warm-cocoa themes for the Indonesia evening palette. abstract final class AppTheme { @@ -80,6 +82,32 @@ abstract final class AppTheme { style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith(_buttonColor), foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary), + minimumSize: const WidgetStatePropertyAll( + Size(0, AppDimens.buttonHeight), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: ButtonStyle( + foregroundColor: WidgetStatePropertyAll( + dark ? AppColors.blueSoft : AppColors.bluePressed, + ), + minimumSize: const WidgetStatePropertyAll( + Size(0, AppDimens.buttonHeight), + ), + side: WidgetStatePropertyAll( + BorderSide( + color: dark ? AppColors.outlineDark : AppColors.outlineLight, + ), + ), + ), + ), + appBarTheme: const AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle.light, + titleTextStyle: TextStyle( + color: AppColors.cream, + fontSize: AppDimens.pageTitle, + fontWeight: FontWeight.w800, ), ), switchTheme: SwitchThemeData( diff --git a/lib/core/widgets/app_bottom_nav.dart b/lib/core/widgets/app_bottom_nav.dart new file mode 100644 index 0000000..c35a033 --- /dev/null +++ b/lib/core/widgets/app_bottom_nav.dart @@ -0,0 +1,109 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../theme/app_colors.dart'; +import '../theme/app_dimens.dart'; + +/// One destination rendered by [AppBottomNav]. +class AppBottomNavItem { + const AppBottomNavItem({ + required this.tabKey, + required this.icon, + required this.selectedIcon, + required this.label, + required this.onTap, + this.selected = false, + }); + + final Key tabKey; + final IconData icon; + final IconData selectedIcon; + final String label; + final VoidCallback onTap; + final bool selected; +} + +/// Shared bottom navigation used by all root tabs and the player fallback. +class AppBottomNav extends StatelessWidget { + const AppBottomNav({ + super.key, + required this.items, + this.backgroundColor = AppColors.bar, + }); + + final List items; + final Color backgroundColor; + + @override + Widget build(BuildContext context) => SafeArea( + top: false, + child: Container( + height: AppDimens.navHeight, + decoration: BoxDecoration( + color: backgroundColor, + border: const Border( + top: BorderSide(color: AppColors.outlineVariantDark), + ), + ), + child: Row( + children: [for (final item in items) _NavDestination(item: item)], + ), + ), + ); +} + +class _NavDestination extends StatelessWidget { + const _NavDestination({required this.item}); + + final AppBottomNavItem item; + + @override + Widget build(BuildContext context) => Expanded( + child: Semantics( + button: true, + selected: item.selected, + label: item.label, + child: InkWell( + key: item.tabKey, + onTap: item.onTap, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: AppDimens.tapTarget), + 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: item.selected ? AppColors.blue : Colors.transparent, + borderRadius: BorderRadius.circular(14), + ), + child: Icon( + item.selected ? item.selectedIcon : item.icon, + size: AppDimens.navIcon, + color: item.selected ? AppColors.onPrimary : AppColors.muted, + ), + ), + const SizedBox(height: 2), + Text( + item.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: AppDimens.navLabel, + fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500, + color: item.selected ? AppColors.blueSoft : AppColors.muted, + ), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/test/home/app_bottom_nav_test.dart b/test/home/app_bottom_nav_test.dart new file mode 100644 index 0000000..6e0d181 --- /dev/null +++ b/test/home/app_bottom_nav_test.dart @@ -0,0 +1,46 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/theme/app_dimens.dart'; +import 'package:fruitshorts/core/widgets/app_bottom_nav.dart'; + +void main() { + testWidgets('should_render_64_high_navigation_and_keep_item_keys', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: AppBottomNav( + items: [ + AppBottomNavItem( + tabKey: const Key('tab-home'), + icon: Icons.movie_outlined, + selectedIcon: Icons.movie_rounded, + label: 'Home', + selected: true, + onTap: () {}, + ), + AppBottomNavItem( + tabKey: const Key('tab-my'), + icon: Icons.person_outline_rounded, + selectedIcon: Icons.person_rounded, + label: 'Me', + onTap: () {}, + ), + ], + ), + ), + ), + ); + + expect(find.byKey(const Key('tab-home')), findsOneWidget); + expect(find.byKey(const Key('tab-my')), findsOneWidget); + expect(find.text('Home'), findsOneWidget); + expect( + tester.getSize(find.byType(AppBottomNav)).height, + AppDimens.navHeight, + ); + }); +} diff --git a/test/theme/app_dimens_test.dart b/test/theme/app_dimens_test.dart new file mode 100644 index 0000000..149b85e --- /dev/null +++ b/test/theme/app_dimens_test.dart @@ -0,0 +1,23 @@ +// Author: fruit.com.cn + +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/theme/app_dimens.dart'; + +void main() { + test('should_expose_the_k20_spacing_and_type_scale', () { + expect(AppDimens.pageHorizontal, 16); + expect(AppDimens.cardRadius, 16); + expect(AppDimens.innerRadius, 12); + expect(AppDimens.caption, 12); + expect(AppDimens.body, 14); + expect(AppDimens.listTitle, 15); + expect(AppDimens.sectionTitle, 16); + expect(AppDimens.pageTitle, 20); + expect(AppDimens.navLabel, 11); + expect(AppDimens.buttonHeight, 48); + expect(AppDimens.primaryButtonHeight, 52); + expect(AppDimens.tapTarget, 44); + expect(AppDimens.navHeight, 64); + expect(AppDimens.progressHeight, 4); + }); +}