checkpoint: focus seed 1.0.1 localized dev switch

This commit is contained in:
2026-06-11 23:35:49 +08:00
parent c51abdfb73
commit d06dc35ee6
72 changed files with 6155 additions and 1390 deletions
+212 -24
View File
@@ -1,52 +1,240 @@
import 'package:flutter/material.dart';
import '../core/ambience.dart';
class AppTheme {
static const seed = Color(0xFF2E7D6B);
static const accent = Color(0xFFF4B740);
static const paper = Color(0xFFF7F8F5);
static const ink = Color(0xFF17211C);
static const leaf = Color(0xFF4D8B63);
static const mint = Color(0xFFA9E8B5);
static const ultramarine = Color(0xFF4E63D8);
static const coral = Color(0xFFEA725E);
static const violet = Color(0xFF7856D8);
static const porcelain = Color(0xFFF7F8F5);
static ThemeData light() {
final scheme = ColorScheme.fromSeed(seedColor: seed, surface: paper);
return _base(scheme);
}
static ThemeData dark() {
final scheme = ColorScheme.fromSeed(
seedColor: seed,
brightness: Brightness.dark,
static ThemeData light(DayPhase phase) {
final surface = switch (phase) {
DayPhase.dawn => const Color(0xFFF3F8F7),
DayPhase.day => porcelain,
DayPhase.dusk => const Color(0xFFFBF5F2),
DayPhase.night => const Color(0xFFF3F5F3),
};
final scheme = ColorScheme.fromSeed(seedColor: leaf).copyWith(
primary: leaf,
onPrimary: Colors.white,
secondary: ultramarine,
tertiary: coral,
surface: surface,
onSurface: ink,
surfaceContainerLowest: Colors.white.withValues(alpha: 0.82),
surfaceContainerLow: Colors.white.withValues(alpha: 0.72),
surfaceContainer: const Color(0xFFF0F3EF),
surfaceContainerHigh: const Color(0xFFE9EEE9),
outline: const Color(0xFFB9C2BA),
outlineVariant: const Color(0xFFDDE3DD),
);
return _base(scheme);
return _base(scheme, phase);
}
static ThemeData _base(ColorScheme scheme) {
return ThemeData(
static ThemeData dark(DayPhase phase) {
final surface = switch (phase) {
DayPhase.dawn => const Color(0xFF141C1B),
DayPhase.day => const Color(0xFF151B18),
DayPhase.dusk => const Color(0xFF1A181A),
DayPhase.night => const Color(0xFF101614),
};
final scheme = ColorScheme.fromSeed(
seedColor: mint,
brightness: Brightness.dark,
).copyWith(
primary: mint,
onPrimary: ink,
secondary: const Color(0xFFB9C2FF),
tertiary: const Color(0xFFFFB4A8),
surface: surface,
onSurface: const Color(0xFFF1F5F1),
surfaceContainerLowest: const Color(0xFF0D1210),
surfaceContainerLow: const Color(0xFF171E1B),
surfaceContainer: const Color(0xFF1C2420),
surfaceContainerHigh: const Color(0xFF243029),
outline: const Color(0xFF7E8D83),
outlineVariant: const Color(0xFF344039),
);
return _base(scheme, phase);
}
static ThemeData _base(ColorScheme scheme, DayPhase phase) {
final base = ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: scheme.surface,
navigationBarTheme: NavigationBarThemeData(
indicatorColor: scheme.primaryContainer,
labelTextStyle: WidgetStateProperty.resolveWith(
(_) => const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
);
final textTheme = base.textTheme.copyWith(
headlineSmall: base.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
height: 1.15,
),
titleLarge: base.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
height: 1.2,
),
titleMedium: base.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
height: 1.25,
),
bodyLarge: base.textTheme.bodyLarge?.copyWith(height: 1.4),
bodyMedium: base.textTheme.bodyMedium?.copyWith(height: 1.4),
labelLarge: base.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
),
);
return base.copyWith(
textTheme: textTheme,
scaffoldBackgroundColor: scheme.surface,
cardTheme: CardTheme(
elevation: 0,
margin: EdgeInsets.zero,
color: scheme.surfaceContainerHighest.withValues(alpha: 0.55),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
color: scheme.surfaceContainerLowest,
shadowColor: ink.withValues(alpha: 0.12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: scheme.outlineVariant),
),
),
navigationBarTheme: NavigationBarThemeData(
height: 68,
elevation: 0,
backgroundColor: scheme.surfaceContainerLowest,
indicatorColor: scheme.primary.withValues(alpha: 0.14),
labelTextStyle: WidgetStateProperty.resolveWith(
(states) => TextStyle(
fontSize: 12,
fontWeight:
states.contains(WidgetState.selected)
? FontWeight.w700
: FontWeight.w500,
),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(48, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: const TextStyle(fontWeight: FontWeight.w700),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(48, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: scheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 13,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: scheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: scheme.outlineVariant),
),
),
chipTheme: base.chipTheme.copyWith(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
side: BorderSide(color: scheme.outlineVariant),
),
dividerTheme: DividerThemeData(
color: scheme.outlineVariant,
space: 1,
thickness: 1,
),
dialogTheme: DialogTheme(
backgroundColor: scheme.surfaceContainerLowest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: scheme.surface,
modalBackgroundColor: scheme.surface,
showDragHandle: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
),
extensions: [FocusSeedVisuals.forPhase(phase, scheme.brightness)],
);
}
}
@immutable
class FocusSeedVisuals extends ThemeExtension<FocusSeedVisuals> {
const FocusSeedVisuals({
required this.ambientTint,
required this.reflection,
required this.success,
required this.shadow,
});
final Color ambientTint;
final Color reflection;
final Color success;
final Color shadow;
static FocusSeedVisuals of(BuildContext context) {
return Theme.of(context).extension<FocusSeedVisuals>()!;
}
factory FocusSeedVisuals.forPhase(DayPhase phase, Brightness brightness) {
final isDark = brightness == Brightness.dark;
final ambientTint = switch (phase) {
DayPhase.dawn => const Color(0xFFA7D9D2),
DayPhase.day => AppTheme.mint,
DayPhase.dusk => const Color(0xFFF1B4A8),
DayPhase.night => const Color(0xFF9A89D8),
};
return FocusSeedVisuals(
ambientTint: ambientTint.withValues(alpha: isDark ? 0.18 : 0.22),
reflection: isDark ? const Color(0xFFBDAAFF) : AppTheme.violet,
success: isDark ? AppTheme.mint : AppTheme.leaf,
shadow: Colors.black.withValues(alpha: isDark ? 0.32 : 0.10),
);
}
@override
FocusSeedVisuals copyWith({
Color? ambientTint,
Color? reflection,
Color? success,
Color? shadow,
}) {
return FocusSeedVisuals(
ambientTint: ambientTint ?? this.ambientTint,
reflection: reflection ?? this.reflection,
success: success ?? this.success,
shadow: shadow ?? this.shadow,
);
}
@override
FocusSeedVisuals lerp(covariant FocusSeedVisuals? other, double t) {
if (other == null) {
return this;
}
return FocusSeedVisuals(
ambientTint: Color.lerp(ambientTint, other.ambientTint, t)!,
reflection: Color.lerp(reflection, other.reflection, t)!,
success: Color.lerp(success, other.success, t)!,
shadow: Color.lerp(shadow, other.shadow, t)!,
);
}
}
+120 -7
View File
@@ -1,19 +1,132 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import '../core/ambience.dart';
import '../data/app_preferences_store.dart';
import '../features/shell/focus_seed_home.dart';
import '../l10n/generated/app_localizations.dart';
import '../models/appearance_preference.dart';
import '../models/language_preference.dart';
import 'app_theme.dart';
class FocusSeedApp extends StatelessWidget {
const FocusSeedApp({super.key});
@visibleForTesting
Locale focusSeedDefaultLocale({bool isRelease = kReleaseMode}) {
return isRelease ? const Locale('en') : const Locale('zh');
}
class FocusSeedApp extends StatefulWidget {
const FocusSeedApp({this.locale, super.key});
final Locale? locale;
@override
State<FocusSeedApp> createState() => _FocusSeedAppState();
}
class _FocusSeedAppState extends State<FocusSeedApp>
with WidgetsBindingObserver {
final _preferencesStore = AppPreferencesStore();
AppearancePreference _appearance = AppearancePreference.system;
LanguagePreference _language = LanguagePreference.defaultLocale;
DayPhase _dayPhase = dayPhaseFor(DateTime.now());
Timer? _phaseTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_loadPreferences();
_schedulePhaseRefresh();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_refreshDayPhase();
}
}
Future<void> _loadPreferences() async {
final appearance = await _preferencesStore.loadAppearance();
final language = await _preferencesStore.loadLanguage();
if (!mounted) {
return;
}
setState(() {
_appearance = appearance;
_language = language;
});
}
Future<void> _setAppearance(AppearancePreference appearance) async {
setState(() => _appearance = appearance);
await _preferencesStore.saveAppearance(appearance);
}
Future<void> _setLanguage(LanguagePreference language) async {
setState(() => _language = language);
await _preferencesStore.saveLanguage(language);
}
void _refreshDayPhase() {
final phase = dayPhaseFor(DateTime.now());
if (phase != _dayPhase && mounted) {
setState(() => _dayPhase = phase);
}
_schedulePhaseRefresh();
}
void _schedulePhaseRefresh() {
_phaseTimer?.cancel();
final now = DateTime.now();
final delay = nextDayPhaseBoundary(now).difference(now);
_phaseTimer = Timer(delay, _refreshDayPhase);
}
ThemeMode get _themeMode => switch (_appearance) {
AppearancePreference.system => ThemeMode.system,
AppearancePreference.light => ThemeMode.light,
AppearancePreference.dark => ThemeMode.dark,
};
Locale get _resolvedLocale {
return widget.locale ?? _language.locale ?? focusSeedDefaultLocale();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Focus Seed',
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
home: const FocusSeedHome(),
theme: AppTheme.light(_dayPhase),
darkTheme: AppTheme.dark(_dayPhase),
themeMode: _themeMode,
themeAnimationDuration: const Duration(milliseconds: 240),
locale: _resolvedLocale,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: FocusSeedHome(
appearance: _appearance,
onAppearanceChanged: _setAppearance,
language: _language,
showLanguageTools: !kReleaseMode && widget.locale == null,
onLanguageChanged: _setLanguage,
),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_phaseTimer?.cancel();
super.dispose();
}
}