feat(ui): 增加视觉 token 与统一导航
This commit is contained in:
1 parent
42ce1d1d84
commit
df5fbe5b08
7 files changed
+260
-1
No files matched your search
+11
-1
@@ -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<LumaDramaApp> {
|
||||
localeResolutionCallback: (deviceLocale, _) =>
|
||||
resolveAppLocale(locale.locale, deviceLocale),
|
||||
debugShowCheckedModeBanner: false,
|
||||
themeMode: ThemeMode.system,
|
||||
themeMode: ThemeMode.dark,
|
||||
theme: AppTheme.light(),
|
||||
darkTheme: AppTheme.dark(),
|
||||
builder: (context, child) => AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: AppColors.bar,
|
||||
systemNavigationBarIconBrightness: Brightness.light,
|
||||
),
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
home: const HomePage(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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<AppBottomNavItem> 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user