feat(flutter): 应用克莱因蓝主题与印尼语适配

This commit is contained in:
Zhou Weidong committed 2026-10-17 15:58:17 +08:00
1 parent e1147cfcba
commit 10d448c8ba
31 files changed
+891 -521

No files matched your search

+15
View File
@@ -3,6 +3,19 @@
import 'package:flutter/widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Resolves the selected or system language to one of the supported app locales.
///
/// Indonesian is the launch-market fallback when a device uses an unsupported
/// language. The legacy `in` code is treated as Indonesian as well.
Locale resolveAppLocale(Locale? selected, Locale? deviceLocale) {
final code = (selected ?? deviceLocale)?.languageCode;
return switch (code) {
'zh' => const Locale('zh'),
'en' => const Locale('en'),
_ => const Locale('id'),
};
}
class LocaleController extends ChangeNotifier {
LocaleController({SharedPreferencesAsync? preferences})
: _preferences = preferences ?? SharedPreferencesAsync();
@@ -17,6 +30,7 @@ class LocaleController extends ChangeNotifier {
_locale = switch (code) {
'en' => const Locale('en'),
'zh' => const Locale('zh'),
'id' => const Locale('id'),
_ => null,
};
notifyListeners();
@@ -29,6 +43,7 @@ class LocaleController extends ChangeNotifier {
_locale = switch (code) {
'en' => const Locale('en'),
'zh' => const Locale('zh'),
'id' => const Locale('id'),
_ => null,
};
notifyListeners();
@@ -0,0 +1,15 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
/// Formats a timestamp with the active Material locale and 12/24-hour setting.
String formatLocalizedDateTime(BuildContext context, DateTime value) {
final localizations = MaterialLocalizations.of(context);
final local = value.toLocal();
final date = localizations.formatCompactDate(local);
final time = localizations.formatTimeOfDay(
TimeOfDay.fromDateTime(local),
alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context),
);
return '$date $time';
}
+11 -2
View File
@@ -2,8 +2,17 @@ import 'package:flutter/material.dart';
/// Shared colors. Blue is the primary action color.
abstract final class AppColors {
static const blue = Color(0xFF2F7BFF);
static const blueSoft = Color(0xFF8FB4FF);
/// Klein Blue primary used by actions and selected states.
static const blue = Color(0xFF002FA7);
/// Pressed/active shade for blue surfaces.
static const bluePressed = Color(0xFF00227A);
/// High-contrast blue for text and icons on dark surfaces.
static const blueSoft = Color(0xFF5B7BE0);
/// Text and icons placed directly on [blue].
static const onPrimary = Color(0xFFFFFFFF);
static const ink = Color(0xFF0B0C10);
static const bar = Color(0xFF101114);
static const card = Color(0xFF17191F);
+106
View File
@@ -0,0 +1,106 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import 'app_colors.dart';
/// App-wide light and dark themes built from the approved Klein Blue palette.
abstract final class AppTheme {
static const _lightBackground = Color(0xFFF8F7FC);
static const _darkBackground = Color(0xFF101019);
/// Light theme for system light mode.
static ThemeData light() => _build(
brightness: Brightness.light,
surface: _lightBackground,
accent: AppColors.blue,
);
/// Dark theme for system dark mode.
static ThemeData dark() => _build(
brightness: Brightness.dark,
surface: _darkBackground,
accent: AppColors.blueSoft,
);
static ThemeData _build({
required Brightness brightness,
required Color surface,
required Color accent,
}) {
final dark = brightness == Brightness.dark;
final scheme = dark
? const ColorScheme.dark(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.blueSoft,
onSecondary: AppColors.ink,
primaryContainer: AppColors.bluePressed,
onPrimaryContainer: AppColors.onPrimary,
secondaryContainer: AppColors.bluePressed,
onSecondaryContainer: AppColors.onPrimary,
surface: _darkBackground,
onSurface: Colors.white,
surfaceTint: AppColors.blueSoft,
inversePrimary: AppColors.blueSoft,
)
: ColorScheme.light(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.blue,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.blueSoft.withValues(alpha: 0.22),
onPrimaryContainer: AppColors.ink,
secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.22),
onSecondaryContainer: AppColors.ink,
surface: _lightBackground,
onSurface: AppColors.ink,
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: surface,
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
sliderTheme: SliderThemeData(
activeTrackColor: accent,
thumbColor: AppColors.onPrimary,
),
tabBarTheme: TabBarThemeData(labelColor: accent, indicatorColor: accent),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: accent),
),
filledButtonTheme: FilledButtonThemeData(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith(_buttonColor),
foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary),
),
),
switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected)
? AppColors.onPrimary
: null,
),
trackColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected) ? accent : null,
),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.bluePressed,
contentTextStyle: const TextStyle(color: AppColors.onPrimary),
),
);
}
static Color _buttonColor(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed) ||
states.contains(WidgetState.hovered) ||
states.contains(WidgetState.focused)) {
return AppColors.bluePressed;
}
return AppColors.blue;
}
}