189 lines
5.7 KiB
Dart
189 lines
5.7 KiB
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 '../data/focus_seed_store.dart';
|
|
import '../features/onboarding/welcome_screen.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 '../widgets/loading_view.dart';
|
|
import 'app_theme.dart';
|
|
|
|
@visibleForTesting
|
|
Locale focusSeedDefaultLocale({bool isRelease = kReleaseMode}) {
|
|
return isRelease ? const Locale('en') : const Locale('zh');
|
|
}
|
|
|
|
class FocusSeedApp extends StatefulWidget {
|
|
const FocusSeedApp({
|
|
this.locale,
|
|
this.focusSeedStore,
|
|
this.showWelcomeOnFirstRun = true,
|
|
super.key,
|
|
});
|
|
|
|
final Locale? locale;
|
|
final FocusSeedStore? focusSeedStore;
|
|
final bool showWelcomeOnFirstRun;
|
|
|
|
@override
|
|
State<FocusSeedApp> createState() => _FocusSeedAppState();
|
|
}
|
|
|
|
class _FocusSeedAppState extends State<FocusSeedApp>
|
|
with WidgetsBindingObserver {
|
|
final _preferencesStore = AppPreferencesStore();
|
|
AppearancePreference _appearance = AppearancePreference.system;
|
|
LanguagePreference _language = LanguagePreference.defaultLocale;
|
|
bool _onboardingComplete = true;
|
|
bool _landingSetupComplete = true;
|
|
bool _showLandingSetup = false;
|
|
bool _preferencesLoaded = false;
|
|
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, language, firstRunProgress) =
|
|
await (
|
|
_preferencesStore.loadAppearance(),
|
|
_preferencesStore.loadLanguage(),
|
|
_preferencesStore.loadFirstRunProgress(),
|
|
).wait;
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_appearance = appearance;
|
|
_language = language;
|
|
_onboardingComplete = firstRunProgress.onboardingComplete;
|
|
_landingSetupComplete = firstRunProgress.landingSetupComplete;
|
|
_showLandingSetup =
|
|
widget.showWelcomeOnFirstRun &&
|
|
firstRunProgress.onboardingComplete &&
|
|
!firstRunProgress.landingSetupComplete;
|
|
_preferencesLoaded = true;
|
|
});
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
Future<void> _completeOnboarding() async {
|
|
setState(() {
|
|
_onboardingComplete = true;
|
|
_showLandingSetup = true;
|
|
});
|
|
await _preferencesStore.saveOnboardingComplete();
|
|
}
|
|
|
|
Future<void> _completeLandingSetup() async {
|
|
setState(() {
|
|
_landingSetupComplete = true;
|
|
_showLandingSetup = false;
|
|
});
|
|
await _preferencesStore.saveLandingSetupComplete();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
|
|
debugShowCheckedModeBanner: false,
|
|
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:
|
|
!_preferencesLoaded
|
|
? const Scaffold(body: SafeArea(child: LoadingView()))
|
|
: widget.showWelcomeOnFirstRun && !_onboardingComplete
|
|
? WelcomeScreen(
|
|
onStart: _completeOnboarding,
|
|
onSkip: _completeOnboarding,
|
|
)
|
|
: FocusSeedHome(
|
|
focusSeedStore: widget.focusSeedStore,
|
|
appearance: _appearance,
|
|
onAppearanceChanged: _setAppearance,
|
|
language: _language,
|
|
showLanguageTools: !kReleaseMode && widget.locale == null,
|
|
onLanguageChanged: _setLanguage,
|
|
showLandingSetupInitially:
|
|
widget.showWelcomeOnFirstRun &&
|
|
(!_landingSetupComplete || _showLandingSetup),
|
|
onLandingSetupComplete: () {
|
|
unawaited(_completeLandingSetup());
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
_phaseTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|