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
+148 -233
View File
@@ -1,269 +1,184 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../core/date_keys.dart';
import '../../data/focus_seed_store.dart';
import '../../models/app_state.dart';
import '../../models/focus_session.dart';
import '../../models/habit.dart';
import '../../models/mood.dart';
import '../../models/reflection_entry.dart';
import '../../services/analytics_provider.dart';
import '../../widgets/dialogs.dart';
import '../../application/focus_seed_controller.dart';
import '../../application/focus_timer_controller.dart';
import '../../l10n/l10n.dart';
import '../../models/appearance_preference.dart';
import '../../models/language_preference.dart';
import '../../widgets/loading_view.dart';
import '../habits/habits_screen.dart';
import '../home/home_screen.dart';
import '../reflection/reflection_screen.dart';
import '../settings/settings_screen.dart';
import '../timer/timer_screen.dart';
import '../journey/journey_screen.dart';
import '../me/me_screen.dart';
import '../today/today_screen.dart';
class FocusSeedHome extends StatefulWidget {
const FocusSeedHome({super.key});
const FocusSeedHome({
required this.appearance,
required this.onAppearanceChanged,
required this.language,
required this.showLanguageTools,
required this.onLanguageChanged,
super.key,
});
final AppearancePreference appearance;
final ValueChanged<AppearancePreference> onAppearanceChanged;
final LanguagePreference language;
final bool showLanguageTools;
final ValueChanged<LanguagePreference> onLanguageChanged;
@override
State<FocusSeedHome> createState() => _FocusSeedHomeState();
}
class _FocusSeedHomeState extends State<FocusSeedHome> {
final _store = FocusSeedStore();
final _analytics = const NoopAnalyticsProvider();
AppState? _state;
FocusSeedController? _controller;
FocusTimerController? _timerController;
int _tabIndex = 0;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final state = await _store.load();
if (!mounted) {
void didChangeDependencies() {
super.didChangeDependencies();
if (_controller != null) {
return;
}
setState(() => _state = state);
}
Future<void> _setStateAndSave(AppState state) async {
setState(() => _state = state);
await _store.save(state);
_controller = FocusSeedController(
defaultHabitTitles: localizedStarterHabits(context.l10n),
);
_timerController = FocusTimerController(
onCompleted: (minutes) async {
final saved = await _controller!.completeFocusSession(minutes);
if (saved) {
await HapticFeedback.mediumImpact();
}
},
);
_controller!.load();
}
@override
Widget build(BuildContext context) {
final state = _state;
return Scaffold(
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
child:
state == null
? const LoadingView()
: SafeArea(
child: IndexedStack(
index: _tabIndex,
final controller = _controller;
final timerController = _timerController;
if (controller == null || timerController == null) {
return const Scaffold(body: SafeArea(child: LoadingView()));
}
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
controller.updateDefaultHabitTitles(
localizedStarterHabits(context.l10n),
);
final state = controller.state;
if (controller.isLoading) {
return const Scaffold(body: SafeArea(child: LoadingView()));
}
if (state == null) {
final l10n = context.l10n;
return Scaffold(
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
HomeScreen(
state: state,
onAnchorChanged:
(anchor) => _setStateAndSave(
state.copyWith(anchor: anchor.trim()),
),
onOpenTimer: () => setState(() => _tabIndex = 1),
onToggleHabit: _toggleHabit,
onAddHabit: _addHabit,
const Icon(Icons.error_outline_rounded, size: 42),
const SizedBox(height: 12),
Text(
_messageForError(l10n, controller.error) ??
l10n.couldNotOpenApp,
textAlign: TextAlign.center,
),
TimerScreen(
onComplete: _completeFocusSession,
onReflect: () => setState(() => _tabIndex = 3),
),
HabitsScreen(
state: state,
onAddHabit: _addHabit,
onRenameHabit: _renameHabit,
onDeleteHabit: _deleteHabit,
onToggleHabit: _toggleHabit,
),
ReflectionScreen(state: state, onSave: _saveReflection),
SettingsScreen(
state: state,
onImport: _importState,
onClear: _clearData,
const SizedBox(height: 16),
FilledButton.icon(
onPressed: controller.load,
icon: const Icon(Icons.refresh_rounded),
label: Text(l10n.tryAgain),
),
],
),
),
),
bottomNavigationBar:
state == null
? null
: NavigationBar(
selectedIndex: _tabIndex,
onDestinationSelected: (index) {
HapticFeedback.selectionClick();
setState(() => _tabIndex = index);
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Today',
),
),
);
}
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
isDark ? Brightness.light : Brightness.dark,
systemNavigationBarColor:
theme.navigationBarTheme.backgroundColor ??
theme.colorScheme.surface,
systemNavigationBarIconBrightness:
isDark ? Brightness.light : Brightness.dark,
),
child: Scaffold(
body: SafeArea(
child: IndexedStack(
index: _tabIndex,
children: [
TodayScreen(
controller: controller,
timerController: timerController,
),
NavigationDestination(
icon: Icon(Icons.timer_outlined),
selectedIcon: Icon(Icons.timer),
label: 'Timer',
),
NavigationDestination(
icon: Icon(Icons.check_circle_outline),
selectedIcon: Icon(Icons.check_circle),
label: 'Habits',
),
NavigationDestination(
icon: Icon(Icons.edit_note_outlined),
selectedIcon: Icon(Icons.edit_note),
label: 'Reflect',
),
NavigationDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: 'Settings',
JourneyScreen(controller: controller),
MeScreen(
controller: controller,
appearance: widget.appearance,
onAppearanceChanged: widget.onAppearanceChanged,
language: widget.language,
showLanguageTools: widget.showLanguageTools,
onLanguageChanged: widget.onLanguageChanged,
),
],
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: _tabIndex,
onDestinationSelected: (index) {
HapticFeedback.selectionClick();
setState(() => _tabIndex = index);
},
destinations: [
NavigationDestination(
icon: const Icon(Icons.eco_outlined),
selectedIcon: const Icon(Icons.eco_rounded),
label: context.l10n.navToday,
),
NavigationDestination(
icon: const Icon(Icons.landscape_outlined),
selectedIcon: const Icon(Icons.landscape_rounded),
label: context.l10n.navJourney,
),
NavigationDestination(
icon: const Icon(Icons.person_outline_rounded),
selectedIcon: const Icon(Icons.person_rounded),
label: context.l10n.navMe,
),
],
),
),
);
},
);
}
Future<void> _toggleHabit(Habit habit) async {
final state = _state;
if (state == null) {
return;
}
final today = todayKey();
final completedDates = Set<String>.from(habit.completedDates);
if (completedDates.contains(today)) {
completedDates.remove(today);
} else {
completedDates.add(today);
await HapticFeedback.lightImpact();
}
final habits =
state.habits
.map(
(item) =>
item.id == habit.id
? habit.copyWith(completedDates: completedDates)
: item,
)
.toList();
_analytics.track('habit_toggled');
await _setStateAndSave(state.copyWith(habits: habits));
}
Future<void> _addHabit(String title) async {
final state = _state;
final clean = title.trim();
if (state == null || clean.isEmpty) {
return;
}
final habit = Habit(
id: 'habit-${DateTime.now().microsecondsSinceEpoch}',
title: clean,
createdAt: DateTime.now(),
completedDates: {},
);
_analytics.track('habit_created');
await _setStateAndSave(state.copyWith(habits: [...state.habits, habit]));
}
Future<void> _renameHabit(Habit habit, String title) async {
final state = _state;
final clean = title.trim();
if (state == null || clean.isEmpty) {
return;
}
final habits =
state.habits
.map(
(item) =>
item.id == habit.id ? item.copyWith(title: clean) : item,
)
.toList();
await _setStateAndSave(state.copyWith(habits: habits));
}
Future<void> _deleteHabit(Habit habit) async {
final state = _state;
if (state == null) {
return;
}
final habits = state.habits.where((item) => item.id != habit.id).toList();
await _setStateAndSave(state.copyWith(habits: habits));
}
Future<void> _completeFocusSession(int minutes) async {
final state = _state;
if (state == null) {
return;
}
final session = FocusSession(
id: 'session-${DateTime.now().microsecondsSinceEpoch}',
minutes: minutes,
completedAt: DateTime.now(),
);
_analytics.track('focus_completed', {'minutes': minutes});
await _setStateAndSave(
state.copyWith(sessions: [...state.sessions, session]),
);
}
Future<void> _saveReflection(String note, Mood mood) async {
final state = _state;
final clean = note.trim();
if (state == null || clean.isEmpty) {
return;
}
final today = todayKey();
final entry = ReflectionEntry(
id: 'reflection-${DateTime.now().microsecondsSinceEpoch}',
dateKey: today,
note: clean,
mood: mood,
createdAt: DateTime.now(),
);
final reflections = [
...state.reflections.where((item) => item.dateKey != today),
entry,
];
_analytics.track('reflection_saved');
await _setStateAndSave(state.copyWith(reflections: reflections));
if (mounted) {
showSnack(context, 'Reflection saved');
}
}
Future<void> _importState(String raw) async {
try {
final imported = AppState.fromJson(
jsonDecode(raw) as Map<String, dynamic>,
);
await _setStateAndSave(imported);
if (mounted) {
showSnack(context, 'Data imported');
}
} on Object {
if (mounted) {
showSnack(context, 'Import failed. Check the JSON and try again.');
}
}
}
Future<void> _clearData() async {
await _store.clear();
await _setStateAndSave(AppState.starter());
if (mounted) {
showSnack(context, 'Local data cleared');
}
@override
void dispose() {
_timerController?.dispose();
_controller?.dispose();
super.dispose();
}
}
String? _messageForError(AppLocalizations l10n, FocusSeedError? error) {
return switch (error) {
FocusSeedError.openFailed => l10n.openFailed,
FocusSeedError.saveFailed => l10n.saveFailed,
null => null,
};
}