release: seal Focus Seed 1.0.0

This commit is contained in:
2026-06-11 00:10:50 +08:00
commit 67ecb15c03
121 changed files with 5253 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
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 '../../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';
class FocusSeedHome extends StatefulWidget {
const FocusSeedHome({super.key});
@override
State<FocusSeedHome> createState() => _FocusSeedHomeState();
}
class _FocusSeedHomeState extends State<FocusSeedHome> {
final _store = FocusSeedStore();
final _analytics = const NoopAnalyticsProvider();
AppState? _state;
int _tabIndex = 0;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final state = await _store.load();
if (!mounted) {
return;
}
setState(() => _state = state);
}
Future<void> _setStateAndSave(AppState state) async {
setState(() => _state = state);
await _store.save(state);
}
@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,
children: [
HomeScreen(
state: state,
onAnchorChanged:
(anchor) => _setStateAndSave(
state.copyWith(anchor: anchor.trim()),
),
onOpenTimer: () => setState(() => _tabIndex = 1),
onToggleHabit: _toggleHabit,
onAddHabit: _addHabit,
),
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,
),
],
),
),
),
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',
),
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',
),
],
),
);
}
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');
}
}
}