206 lines
5.6 KiB
Dart
206 lines
5.6 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.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';
|
|
|
|
enum FocusSeedError { openFailed, saveFailed }
|
|
|
|
class FocusSeedController extends ChangeNotifier {
|
|
FocusSeedController({
|
|
FocusSeedStore? store,
|
|
AnalyticsProvider? analytics,
|
|
List<String> defaultHabitTitles = const [],
|
|
}) : _store = store ?? FocusSeedStore(),
|
|
_analytics = analytics ?? const NoopAnalyticsProvider(),
|
|
_defaultHabitTitles = List.unmodifiable(defaultHabitTitles);
|
|
|
|
final FocusSeedStore _store;
|
|
final AnalyticsProvider _analytics;
|
|
List<String> _defaultHabitTitles;
|
|
|
|
AppState? _state;
|
|
bool _isLoading = true;
|
|
bool _isSaving = false;
|
|
FocusSeedError? _error;
|
|
|
|
AppState? get state => _state;
|
|
bool get isLoading => _isLoading;
|
|
bool get isSaving => _isSaving;
|
|
FocusSeedError? get error => _error;
|
|
|
|
void updateDefaultHabitTitles(List<String> titles) {
|
|
_defaultHabitTitles = List.unmodifiable(titles);
|
|
}
|
|
|
|
Future<void> load() async {
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
try {
|
|
_state = await _store.load(defaultHabitTitles: _defaultHabitTitles);
|
|
} on Object {
|
|
_error = FocusSeedError.openFailed;
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<bool> setTodayAnchor(String value) async {
|
|
final current = _state;
|
|
final anchor = value.trim();
|
|
if (current == null || anchor.isEmpty) {
|
|
return false;
|
|
}
|
|
_analytics.track('intention_saved');
|
|
return _commit(current.copyWith(anchor: anchor, anchorDateKey: todayKey()));
|
|
}
|
|
|
|
Future<bool> toggleHabit(Habit habit) async {
|
|
final current = _state;
|
|
if (current == null) {
|
|
return false;
|
|
}
|
|
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 = current.habits
|
|
.map(
|
|
(item) =>
|
|
item.id == habit.id
|
|
? habit.copyWith(completedDates: completedDates)
|
|
: item,
|
|
)
|
|
.toList(growable: false);
|
|
_analytics.track('habit_toggled');
|
|
return _commit(current.copyWith(habits: habits));
|
|
}
|
|
|
|
Future<bool> addHabit(String value) async {
|
|
final current = _state;
|
|
final title = value.trim();
|
|
if (current == null || title.isEmpty) {
|
|
return false;
|
|
}
|
|
final now = DateTime.now();
|
|
final habit = Habit(
|
|
id: 'habit-${now.microsecondsSinceEpoch}',
|
|
title: title,
|
|
createdAt: now,
|
|
completedDates: const {},
|
|
);
|
|
_analytics.track('habit_created');
|
|
return _commit(current.copyWith(habits: [...current.habits, habit]));
|
|
}
|
|
|
|
Future<bool> renameHabit(Habit habit, String value) async {
|
|
final current = _state;
|
|
final title = value.trim();
|
|
if (current == null || title.isEmpty) {
|
|
return false;
|
|
}
|
|
final habits = current.habits
|
|
.map((item) => item.id == habit.id ? item.copyWith(title: title) : item)
|
|
.toList(growable: false);
|
|
return _commit(current.copyWith(habits: habits));
|
|
}
|
|
|
|
Future<bool> deleteHabit(Habit habit) async {
|
|
final current = _state;
|
|
if (current == null) {
|
|
return false;
|
|
}
|
|
final habits = current.habits
|
|
.where((item) => item.id != habit.id)
|
|
.toList(growable: false);
|
|
return _commit(current.copyWith(habits: habits));
|
|
}
|
|
|
|
Future<bool> completeFocusSession(int minutes) async {
|
|
final current = _state;
|
|
if (current == null) {
|
|
return false;
|
|
}
|
|
final now = DateTime.now();
|
|
final session = FocusSession(
|
|
id: 'session-${now.microsecondsSinceEpoch}',
|
|
minutes: minutes,
|
|
completedAt: now,
|
|
);
|
|
_analytics.track('focus_completed', {'minutes': minutes});
|
|
return _commit(current.copyWith(sessions: [...current.sessions, session]));
|
|
}
|
|
|
|
Future<bool> saveReflection(String value, Mood mood) async {
|
|
final current = _state;
|
|
final note = value.trim();
|
|
if (current == null || note.isEmpty) {
|
|
return false;
|
|
}
|
|
final today = todayKey();
|
|
final now = DateTime.now();
|
|
final entry = ReflectionEntry(
|
|
id: 'reflection-${now.microsecondsSinceEpoch}',
|
|
dateKey: today,
|
|
note: note,
|
|
mood: mood,
|
|
createdAt: now,
|
|
);
|
|
final reflections = [
|
|
...current.reflections.where((item) => item.dateKey != today),
|
|
entry,
|
|
];
|
|
_analytics.track('reflection_saved');
|
|
return _commit(current.copyWith(reflections: reflections));
|
|
}
|
|
|
|
Future<bool> importState(String raw) async {
|
|
try {
|
|
final decoded = jsonDecode(raw);
|
|
if (decoded is! Map<String, dynamic>) {
|
|
return false;
|
|
}
|
|
return _commit(AppState.fromJson(decoded));
|
|
} on Object {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<bool> clearData() async {
|
|
return _commit(AppState.starter(defaultHabitTitles: _defaultHabitTitles));
|
|
}
|
|
|
|
Future<bool> _commit(AppState nextState) async {
|
|
final previousState = _state;
|
|
_state = nextState;
|
|
_isSaving = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
try {
|
|
await _store.save(nextState);
|
|
return true;
|
|
} on Object {
|
|
_state = previousState;
|
|
_error = FocusSeedError.saveFailed;
|
|
return false;
|
|
} finally {
|
|
_isSaving = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|