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
+35 -6
View File
@@ -5,26 +5,34 @@ import 'reflection_entry.dart';
class AppState {
const AppState({
required this.anchor,
required this.anchorDateKey,
required this.habits,
required this.sessions,
required this.reflections,
});
final String anchor;
final String anchorDateKey;
final List<Habit> habits;
final List<FocusSession> sessions;
final List<ReflectionEntry> reflections;
bool get isFreshStart => habits.isEmpty && sessions.isEmpty && anchor.isEmpty;
String activeAnchor(String dateKey) {
return anchorDateKey == dateKey ? anchor : '';
}
AppState copyWith({
String? anchor,
String? anchorDateKey,
List<Habit>? habits,
List<FocusSession>? sessions,
List<ReflectionEntry>? reflections,
}) {
return AppState(
anchor: anchor ?? this.anchor,
anchorDateKey: anchorDateKey ?? this.anchorDateKey,
habits: habits ?? this.habits,
sessions: sessions ?? this.sessions,
reflections: reflections ?? this.reflections,
@@ -33,8 +41,9 @@ class AppState {
Map<String, dynamic> toJson() {
return {
'version': 1,
'version': 2,
'anchor': anchor,
'anchorDateKey': anchorDateKey,
'habits': habits.map((habit) => habit.toJson()).toList(),
'sessions': sessions.map((session) => session.toJson()).toList(),
'reflections':
@@ -43,8 +52,12 @@ class AppState {
}
static AppState fromJson(Map<String, dynamic> json) {
final anchor = (json['anchor'] as String?) ?? '';
return AppState(
anchor: (json['anchor'] as String?) ?? '',
anchor: anchor,
anchorDateKey:
(json['anchorDateKey'] as String?) ??
(anchor.isEmpty ? '' : _currentDateKey()),
habits: _jsonObjects(json['habits']).map(Habit.fromJson).toList(),
sessions:
_jsonObjects(json['sessions']).map(FocusSession.fromJson).toList(),
@@ -62,26 +75,35 @@ class AppState {
return value.whereType<Map<String, dynamic>>();
}
static AppState starter() {
static AppState starter({List<String>? defaultHabitTitles}) {
final now = DateTime.now();
final titles =
defaultHabitTitles?.length == 3
? defaultHabitTitles!
: const [
'Plan one clear task',
'Focus for 25 minutes',
'Write one-line reflection',
];
return AppState(
anchor: '',
anchorDateKey: _currentDateKey(now),
habits: [
Habit(
id: 'habit-${now.microsecondsSinceEpoch}-1',
title: 'Plan one clear task',
title: titles[0],
createdAt: now,
completedDates: {},
),
Habit(
id: 'habit-${now.microsecondsSinceEpoch}-2',
title: 'Focus for 25 minutes',
title: titles[1],
createdAt: now,
completedDates: {},
),
Habit(
id: 'habit-${now.microsecondsSinceEpoch}-3',
title: 'Write a one-line reflection',
title: titles[2],
createdAt: now,
completedDates: {},
),
@@ -90,4 +112,11 @@ class AppState {
reflections: const [],
);
}
static String _currentDateKey([DateTime? value]) {
final date = value ?? DateTime.now();
return '${date.year.toString().padLeft(4, '0')}-'
'${date.month.toString().padLeft(2, '0')}-'
'${date.day.toString().padLeft(2, '0')}';
}
}
@@ -0,0 +1,8 @@
enum AppearancePreference { system, light, dark }
AppearancePreference appearancePreferenceFromName(String? name) {
return AppearancePreference.values.firstWhere(
(preference) => preference.name == name,
orElse: () => AppearancePreference.system,
);
}
+29
View File
@@ -0,0 +1,29 @@
import '../core/date_keys.dart';
import 'app_state.dart';
enum GrowthStage { seed, sprout, leaves, flower }
GrowthStage growthStageFor(AppState state, String dateKey) {
if (state.reflections.any((entry) => entry.dateKey == dateKey)) {
return GrowthStage.flower;
}
final hasFocus = minutesForDate(state.sessions, dateKey) > 0;
final hasHabit = state.habits.any(
(habit) => habit.completedDates.contains(dateKey),
);
if (hasFocus || hasHabit) {
return GrowthStage.leaves;
}
return state.activeAnchor(dateKey).isEmpty
? GrowthStage.seed
: GrowthStage.sprout;
}
extension GrowthStagePresentation on GrowthStage {
String get assetPath => switch (this) {
GrowthStage.seed => 'assets/images/growth/growth-seed.png',
GrowthStage.sprout => 'assets/images/growth/growth-sprout.png',
GrowthStage.leaves => 'assets/images/growth/growth-leaves.png',
GrowthStage.flower => 'assets/images/growth/growth-flower.png',
};
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
enum LanguagePreference { defaultLocale, english, chinese }
LanguagePreference languagePreferenceFromName(String? name) {
return LanguagePreference.values.firstWhere(
(preference) => preference.name == name,
orElse: () => LanguagePreference.defaultLocale,
);
}
extension LanguagePreferenceLocale on LanguagePreference {
Locale? get locale => switch (this) {
LanguagePreference.defaultLocale => null,
LanguagePreference.english => const Locale('en'),
LanguagePreference.chinese => const Locale('zh'),
};
}
+5 -6
View File
@@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
enum Mood {
calm('Calm', Icons.spa_outlined),
clear('Clear', Icons.wb_sunny_outlined),
proud('Proud', Icons.emoji_events_outlined),
tired('Tired', Icons.bedtime_outlined);
calm(Icons.spa_outlined),
clear(Icons.wb_sunny_outlined),
proud(Icons.emoji_events_outlined),
tired(Icons.bedtime_outlined);
const Mood(this.label, this.icon);
const Mood(this.icon);
final String label;
final IconData icon;
}
+49
View File
@@ -0,0 +1,49 @@
import '../core/date_keys.dart';
import 'app_state.dart';
import 'growth_stage.dart';
class TodayProgress {
const TodayProgress({
required this.dateKey,
required this.intention,
required this.focusMinutes,
required this.completedHabits,
required this.totalHabits,
required this.hasReflection,
required this.growthStage,
});
factory TodayProgress.fromState(AppState state, String dateKey) {
return TodayProgress(
dateKey: dateKey,
intention: state.activeAnchor(dateKey),
focusMinutes: minutesForDate(state.sessions, dateKey),
completedHabits:
state.habits
.where((habit) => habit.completedDates.contains(dateKey))
.length,
totalHabits: state.habits.length,
hasReflection: state.reflections.any((entry) => entry.dateKey == dateKey),
growthStage: growthStageFor(state, dateKey),
);
}
final String dateKey;
final String intention;
final int focusMinutes;
final int completedHabits;
final int totalHabits;
final bool hasReflection;
final GrowthStage growthStage;
bool get hasIntention => intention.isNotEmpty;
bool get hasFocus => focusMinutes > 0;
bool get hasHabits => completedHabits > 0;
int get completedSteps =>
[
hasIntention,
hasFocus,
hasHabits,
hasReflection,
].where((complete) => complete).length;
}