chore(release): 合并 develop 到 release/v1.0.0

This commit is contained in:
Zhou Weidong authored and trae-solo-agent committed 2026-10-19 21:39:21 +08:00
commit 35364c949a
69 files changed
+6115 -2941

No files matched your search

+6 -6
View File
@@ -39,11 +39,11 @@ abstract final class ServerEndpoint {
/// In release builds this returns immediately without reading preferences.
static Future<void> load({
bool debug = kDebugMode,
SharedPreferences? preferences,
SharedPreferencesAsync? preferences,
}) async {
if (!debug) return;
try {
final prefs = preferences ?? await SharedPreferences.getInstance();
final prefs = preferences ?? SharedPreferencesAsync();
final stored = await prefs.getString(preferenceKey);
AppConfig.applyRuntimeBaseUrl(
resolve(
@@ -60,12 +60,12 @@ abstract final class ServerEndpoint {
/// Persists a normalized runtime endpoint.
static Future<bool> save(
String raw, {
SharedPreferences? preferences,
SharedPreferencesAsync? preferences,
}) async {
final normalized = normalize(raw);
if (normalized == null) return false;
try {
await (preferences ?? await SharedPreferences.getInstance()).setString(
await (preferences ?? SharedPreferencesAsync()).setString(
preferenceKey,
normalized,
);
@@ -76,9 +76,9 @@ abstract final class ServerEndpoint {
}
/// Removes the persisted runtime endpoint.
static Future<void> clear({SharedPreferences? preferences}) async {
static Future<void> clear({SharedPreferencesAsync? preferences}) async {
try {
await (preferences ?? await SharedPreferences.getInstance()).remove(preferenceKey);
await (preferences ?? SharedPreferencesAsync()).remove(preferenceKey);
} catch (_) {
// Clearing an optional developer override is best effort.
}
@@ -23,7 +23,7 @@ class AuthSessionStore {
Future<String> deviceId() async {
if (_cachedDeviceId != null) return _cachedDeviceId!;
final prefs = await SharedPreferences.getInstance();
final prefs = SharedPreferencesAsync();
var value = await prefs.getString(_kDeviceId) ?? '';
if (value.isEmpty) {
final random = Random();
@@ -37,7 +37,7 @@ class AuthSessionStore {
}
Future<AuthSession?> read() async {
final prefs = await SharedPreferences.getInstance();
final prefs = SharedPreferencesAsync();
final accessToken = await prefs.getString(_kAccessToken) ?? '';
final refreshToken = await prefs.getString(_kRefreshToken) ?? '';
if (accessToken.isEmpty || refreshToken.isEmpty) return null;
@@ -58,14 +58,14 @@ class AuthSessionStore {
}
Future<void> save(AuthSession session) async {
final prefs = await SharedPreferences.getInstance();
final prefs = SharedPreferencesAsync();
await prefs.setString(_kAccessToken, session.accessToken);
await prefs.setString(_kRefreshToken, session.refreshToken);
await prefs.setString(_kUser, jsonEncode(session.user.toJson()));
}
Future<void> clear() async {
final prefs = await SharedPreferences.getInstance();
final prefs = SharedPreferencesAsync();
await prefs.remove(_kAccessToken);
await prefs.remove(_kRefreshToken);
await prefs.remove(_kUser);
+55 -27
View File
@@ -2,26 +2,31 @@
import 'package:flutter/material.dart';
/// Evening palette for the Indonesia launch.
/// Obsidian Flare palette from `docs/design/README.md` and screens 01/02.
///
/// Chili is the action color. Gold marks coins, rewards, and playback
/// progress. Cocoa surfaces keep the app in night-market light instead of
/// a cold utility screen.
/// Existing semantic names (`blue`, `blueSoft`, `ink`, ...) are kept so page
/// code does not need a mass rename; their values now follow the design tokens.
abstract final class AppColors {
/// Chili fill for buttons, selected chips, and brand marks.
static const blue = Color(0xFFC4322A);
/// Primary CTA, selected navigation, playback progress.
static const blue = Color(0xFFFF5B3D);
static const primary = blue;
/// Pressed chili.
static const bluePressed = Color(0xFF8E221C);
/// Pressed state for the primary CTA.
static const bluePressed = Color(0xFFFF4321);
/// Coral for text and icons sitting on dark surfaces.
static const blueSoft = Color(0xFFFF8A72);
/// Secondary accent for tags, locks, and highlighted icons.
static const blueSoft = Color(0xFF9278EA);
static const secondary = blueSoft;
static const lavender = blueSoft;
/// Coins, rewards, and the playback progress line.
static const gold = Color(0xFFF6C453);
/// Coins and rewards.
static const gold = Color(0xFFFBBF24);
/// Favorite/like heart without competing with the chili primary action.
static const favorite = Color(0xFFD74A7D);
/// Success and free-state accents.
static const success = Color(0xFF34D399);
/// Liked hearts use the primary accent.
static const favorite = blue;
/// Text placed on [gold].
static const onGold = Color(0xFF2A1810);
@@ -30,23 +35,46 @@ abstract final class AppColors {
static const onPrimary = Color(0xFFFFFFFF);
/// Page background.
static const ink = Color(0xFF100C0A);
static const ink = Color(0xFF0B0C10);
/// Bottom bar.
static const bar = Color(0xFF1A1412);
/// Bottom navigation background (rendered at 95% with blur).
static const bar = Color(0xFF0D0D11);
/// Cards lifted off [ink].
static const card = Color(0xFF241C19);
static const card = Color(0xFF1B1C21);
/// Chips and nested rows.
static const raised = Color(0xFF322824);
/// Alternate card surface from the mobile preview.
static const cardAlt = Color(0xFF1C1D21);
/// Primary text on dark surfaces.
static const cream = Color(0xFFF8F1EA);
/// Floating surfaces, inputs, and nested rows.
static const raised = Color(0xFF1C1D21);
static const quick = Color(0xFF202126);
static const input = Color(0xFF25252A);
static const goldCard = Color(0xFF201B1E);
static const muted = Color(0xFFB7A59C);
static const outlineDark = Color(0xFF4A3C36);
static const outlineVariantDark = Color(0xFF2A221F);
static const outlineLight = Color(0xFF8C7A72);
static const outlineVariantLight = Color(0xFFE4D5CC);
/// Body text on dark surfaces.
static const cream = Color(0xFFE6E1E3);
/// Titles and high-emphasis text.
static const onSurface = Color(0xFFFFFFFF);
/// Secondary text.
static const muted = Color(0xFFD2CDD0);
/// Weak hints and timestamps.
static const subtle = Color(0xFF9A96A3);
/// Soft orange used for continue labels and secondary times.
static const accent = Color(0xFFFFA490);
/// 8% white border used by cards and inputs.
static const border = Color(0x14FFFFFF);
/// 14% white border used by glass controls.
static const glassBorder = Color(0x24FFFFFF);
static const outlineDark = Color(0xFF2A2C36);
static const outlineVariantDark = Color(0xFF242329);
static const outlineLight = Color(0xFF8E8E93);
static const outlineVariantLight = Color(0xFFE4E4E7);
}
+26 -8
View File
@@ -1,6 +1,6 @@
// Author: fruit.com.cn
/// K20 cross-page spacing, type, radius, and control sizes.
/// Cross-page spacing, type, radius, and control sizes.
abstract final class AppDimens {
static const double spaceXs = 4;
static const double spaceSm = 8;
@@ -9,12 +9,17 @@ abstract final class AppDimens {
static const double spaceXl = 24;
static const double pageHorizontal = 16;
static const double cardRadius = 16;
static const double cardRadius = 12;
static const double authCardRadius = 20;
static const double cardPadding = 16;
static const double innerRadius = 12;
static const double innerRadius = 10;
static const double innerPadding = 12;
static const double pillRadius = 20;
static const double glassSmallRadius = 12;
static const double glassRadius = 14;
static const double glassCardRadius = 14;
static const double buttonRadius = 9;
static const double coverRadius = 10;
static const double caption = 12;
static const double emptyStateSubtitle = 12.5;
@@ -24,16 +29,29 @@ abstract final class AppDimens {
static const double sectionTitle = 16;
static const double pageTitle = 20;
static const double detailHeadline = 24;
static const double navLabel = 11;
static const double detailHeadlineSmall = 18;
static const double previewCardTitle = 13;
static const double previewSectionTitle = 14;
static const double chipFont = 10;
static const double navLabel = 9;
static const double buttonHeight = 48;
static const double primaryButtonHeight = 52;
static const double buttonCompact = 32;
static const double buttonSmall = 34;
static const double buttonMedium = 40;
static const double buttonLarge = 44;
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 navHeight = 54;
static const double navIcon = 17;
static const double progressHeight = 3;
static const double progressRadius = 2;
static const double progressLineHeight = 3;
static const double searchHeight = 44;
static const double navDot = 3;
static const double glassBlur = 24;
static const double accountCardHeight = 64;
static const double accountIcon = 18;
+37 -13
View File
@@ -6,9 +6,10 @@ import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_dimens.dart';
/// Light paper and warm-cocoa themes for the Indonesia evening palette.
/// Obsidian Flare themes. Dark is the product default; light mirrors the same
/// orange primary for system-light devices.
abstract final class AppTheme {
static const _lightBackground = Color(0xFFFFF6F1);
static const _lightBackground = Color(0xFFF7F7F8);
static const _darkBackground = AppColors.ink;
/// Light theme for system light mode.
@@ -22,7 +23,7 @@ abstract final class AppTheme {
static ThemeData dark() => _build(
brightness: Brightness.dark,
surface: _darkBackground,
accent: AppColors.blueSoft,
accent: AppColors.blue,
);
static ThemeData _build({
@@ -35,27 +36,27 @@ abstract final class AppTheme {
? const ColorScheme.dark(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.blueSoft,
onSecondary: AppColors.ink,
secondary: AppColors.secondary,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.bluePressed,
onPrimaryContainer: AppColors.onPrimary,
secondaryContainer: AppColors.bluePressed,
secondaryContainer: AppColors.raised,
onSecondaryContainer: AppColors.onPrimary,
surface: _darkBackground,
onSurface: AppColors.cream,
onSurface: AppColors.onSurface,
outline: AppColors.outlineDark,
outlineVariant: AppColors.outlineVariantDark,
surfaceTint: AppColors.blueSoft,
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
)
: ColorScheme.light(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.blue,
secondary: AppColors.secondary,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.blueSoft.withValues(alpha: 0.22),
primaryContainer: AppColors.blue.withValues(alpha: 0.14),
onPrimaryContainer: AppColors.ink,
secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.22),
secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.14),
onSecondaryContainer: AppColors.ink,
surface: _lightBackground,
onSurface: AppColors.ink,
@@ -64,11 +65,12 @@ abstract final class AppTheme {
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
);
return ThemeData(
final base = ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: surface,
fontFamily: 'PlusJakartaSans',
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
sliderTheme: SliderThemeData(
activeTrackColor: accent,
@@ -105,7 +107,8 @@ abstract final class AppTheme {
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle.light,
titleTextStyle: TextStyle(
color: AppColors.cream,
color: AppColors.onSurface,
fontFamily: 'Outfit',
fontSize: AppDimens.pageTitle,
fontWeight: FontWeight.w800,
),
@@ -125,8 +128,29 @@ abstract final class AppTheme {
contentTextStyle: const TextStyle(color: AppColors.onPrimary),
),
);
final textTheme = base.textTheme
.apply(
fontFamily: 'PlusJakartaSans',
bodyColor: dark ? AppColors.cream : AppColors.ink,
displayColor: dark ? AppColors.onSurface : AppColors.ink,
)
.copyWith(
displayLarge: _display(base.textTheme.displayLarge, dark),
displayMedium: _display(base.textTheme.displayMedium, dark),
displaySmall: _display(base.textTheme.displaySmall, dark),
headlineLarge: _display(base.textTheme.headlineLarge, dark),
headlineMedium: _display(base.textTheme.headlineMedium, dark),
headlineSmall: _display(base.textTheme.headlineSmall, dark),
titleLarge: _display(base.textTheme.titleLarge, dark),
);
return base.copyWith(textTheme: textTheme);
}
static TextStyle? _display(TextStyle? style, bool dark) => style?.copyWith(
fontFamily: 'Outfit',
color: dark ? AppColors.onSurface : AppColors.ink,
);
static Color _buttonColor(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed) ||
states.contains(WidgetState.hovered) ||
+30 -25
View File
@@ -1,5 +1,7 @@
// Author: fruit.com.cn
import 'dart:ui';
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
@@ -24,7 +26,7 @@ class AppBottomNavItem {
final bool selected;
}
/// Shared bottom navigation used by all root tabs and the player fallback.
/// Shared 3-tab navigation used by all root tabs and the player fallback.
class AppBottomNav extends StatelessWidget {
const AppBottomNav({
super.key,
@@ -38,20 +40,25 @@ class AppBottomNav extends StatelessWidget {
final Key? barKey;
@override
Widget build(BuildContext context) => SafeArea(
top: false,
child: Container(
key: barKey,
height: AppDimens.navHeight,
decoration: BoxDecoration(
color: backgroundColor,
border: const Border(
top: BorderSide(color: AppColors.outlineVariantDark),
Widget build(BuildContext context) => ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: SafeArea(
top: false,
child: Container(
key: barKey,
height: AppDimens.navHeight,
decoration: BoxDecoration(
color: backgroundColor.withValues(alpha: 0.95),
border: const Border(
top: BorderSide(color: AppColors.outlineVariantDark),
),
),
child: Row(
children: [for (final item in items) _NavDestination(item: item)],
),
),
),
child: Row(
children: [for (final item in items) _NavDestination(item: item)],
),
),
);
}
@@ -71,25 +78,20 @@ class _NavDestination extends StatelessWidget {
key: item.tabKey,
onTap: item.onTap,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: AppDimens.tapTarget),
constraints: const BoxConstraints(minHeight: 48),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
AnimatedScale(
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(AppDimens.pillRadius),
),
scale: item.selected ? 1 : 0.96,
child: Icon(
item.selected ? item.selectedIcon : item.icon,
size: AppDimens.navIcon,
color: item.selected ? AppColors.onPrimary : AppColors.muted,
color: item.selected
? AppColors.blue
: Colors.white.withValues(alpha: 0.45),
),
),
const SizedBox(height: 2),
@@ -100,7 +102,10 @@ class _NavDestination extends StatelessWidget {
style: TextStyle(
fontSize: AppDimens.navLabel,
fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500,
color: item.selected ? AppColors.blueSoft : AppColors.muted,
color: item.selected
? AppColors.blue
: Colors.white.withValues(alpha: 0.45),
letterSpacing: 0,
),
),
],
+54
View File
@@ -0,0 +1,54 @@
// Author: fruit.com.cn
import 'dart:ui';
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
/// Dark glass surface used by the home info card and floating controls.
class GlassPanel extends StatelessWidget {
const GlassPanel({
super.key,
required this.child,
this.radius = AppDimens.glassRadius,
this.blur = 10,
this.padding,
this.borderColor = AppColors.glassBorder,
this.gradient,
});
final Widget child;
final double radius;
final double blur;
final EdgeInsetsGeometry? padding;
final Color borderColor;
final Gradient? gradient;
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(radius);
final surface = DecoratedBox(
decoration: BoxDecoration(
borderRadius: borderRadius,
border: Border.all(color: borderColor),
gradient:
gradient ??
const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0x24FFFFFF), Color(0x0DFFFFFF)],
),
),
child: Padding(padding: padding ?? EdgeInsets.zero, child: child),
);
return ClipRRect(
borderRadius: borderRadius,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
child: surface,
),
);
}
}