chore(release): 合并 develop 到 release/v1.0.0
This commit is contained in:
commit
35364c949a
69 files changed
+6115
-2941
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user