checkpoint: focus seed 1.0.1 localized dev switch
This commit is contained in:
+212
-24
@@ -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)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user