首次远程推送
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../core/date_keys.dart';
|
||||
import '../core/focus_durations.dart';
|
||||
import '../data/focus_seed_store.dart';
|
||||
import '../models/app_state.dart';
|
||||
import '../models/focus_session.dart';
|
||||
@@ -55,12 +56,18 @@ class FocusSeedController extends ChangeNotifier {
|
||||
|
||||
Future<bool> setTodayAnchor(String value) async {
|
||||
final current = _state;
|
||||
final anchor = value.trim();
|
||||
if (current == null || anchor.isEmpty) {
|
||||
final title = value.trim();
|
||||
if (current == null || title.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
_analytics.track('intention_saved');
|
||||
return _commit(current.copyWith(anchor: anchor, anchorDateKey: todayKey()));
|
||||
return _commit(
|
||||
current.copyWith(
|
||||
anchor: title,
|
||||
anchorDateKey: todayKey(),
|
||||
selectedActionId: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> toggleHabit(Habit habit) async {
|
||||
@@ -128,46 +135,86 @@ class FocusSeedController extends ChangeNotifier {
|
||||
return _commit(current.copyWith(habits: habits));
|
||||
}
|
||||
|
||||
Future<bool> completeFocusSession(int minutes) async {
|
||||
Future<bool> completeFocusSession(int minutes, {int? actualSeconds}) async {
|
||||
final current = _state;
|
||||
if (current == null) {
|
||||
return false;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final today = todayKey();
|
||||
final anchor = current.activeAnchor(today);
|
||||
final session = FocusSession(
|
||||
id: 'session-${now.microsecondsSinceEpoch}',
|
||||
minutes: minutes,
|
||||
actualSeconds: actualSeconds,
|
||||
completedAt: now,
|
||||
intention: anchor,
|
||||
actionTitleSnapshot: anchor,
|
||||
);
|
||||
_analytics.track('focus_completed', {
|
||||
'minutes': minutes,
|
||||
'actualSeconds': session.actualSeconds,
|
||||
});
|
||||
return _commit(
|
||||
current.copyWith(
|
||||
anchor: anchor,
|
||||
anchorDateKey: today,
|
||||
sessions: [...current.sessions, session],
|
||||
),
|
||||
);
|
||||
_analytics.track('focus_completed', {'minutes': minutes});
|
||||
return _commit(current.copyWith(sessions: [...current.sessions, session]));
|
||||
}
|
||||
|
||||
Future<bool> saveReflection(String value, Mood mood) async {
|
||||
return saveReflectionForDate(todayKey(), value, mood);
|
||||
}
|
||||
|
||||
Future<bool> saveReflectionForDate(
|
||||
String dateKey,
|
||||
String value,
|
||||
Mood mood,
|
||||
) async {
|
||||
final current = _state;
|
||||
final note = value.trim();
|
||||
if (current == null || note.isEmpty) {
|
||||
final trimmedNote = value.trim();
|
||||
final targetDateKey = dateKey.trim();
|
||||
if (current == null || targetDateKey.isEmpty || trimmedNote.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final today = todayKey();
|
||||
final now = DateTime.now();
|
||||
final entry = ReflectionEntry(
|
||||
id: 'reflection-${now.microsecondsSinceEpoch}',
|
||||
dateKey: today,
|
||||
note: note,
|
||||
dateKey: targetDateKey,
|
||||
note: trimmedNote,
|
||||
mood: mood,
|
||||
createdAt: now,
|
||||
);
|
||||
final reflections = [
|
||||
...current.reflections.where((item) => item.dateKey != today),
|
||||
...current.reflections.where((item) => item.dateKey != targetDateKey),
|
||||
entry,
|
||||
];
|
||||
_analytics.track('reflection_saved');
|
||||
return _commit(current.copyWith(reflections: reflections));
|
||||
}
|
||||
|
||||
Future<bool> setDefaultFocusMinutes(int minutes) async {
|
||||
final current = _state;
|
||||
if (current == null || !isFocusDurationPreset(minutes)) {
|
||||
return false;
|
||||
}
|
||||
if (current.defaultFocusMinutes == minutes) {
|
||||
return true;
|
||||
}
|
||||
_analytics.track('default_focus_minutes_changed', {'minutes': minutes});
|
||||
return _commit(current.copyWith(defaultFocusMinutes: minutes));
|
||||
}
|
||||
|
||||
Future<bool> clearData() async {
|
||||
return _commit(AppState.starter(defaultHabitTitles: _defaultHabitTitles));
|
||||
final current = _state;
|
||||
return _commit(
|
||||
AppState.starter(defaultHabitTitles: _defaultHabitTitles).copyWith(
|
||||
defaultFocusMinutes:
|
||||
current?.defaultFocusMinutes ?? defaultFocusDurationMinutes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> clearToday() async {
|
||||
@@ -190,12 +237,22 @@ class FocusSeedController extends ChangeNotifier {
|
||||
final reflections = current.reflections
|
||||
.where((reflection) => reflection.dateKey != today)
|
||||
.toList(growable: false);
|
||||
final actions = current.actions
|
||||
.where((action) => action.dateKey != today)
|
||||
.toList(growable: false);
|
||||
final progressNotes = current.progressNotes
|
||||
.where((note) => note.dateKey != today)
|
||||
.toList(growable: false);
|
||||
_analytics.track('today_cleared');
|
||||
return _commit(
|
||||
current.copyWith(
|
||||
anchor: current.anchorDateKey == today ? '' : current.anchor,
|
||||
selectedActionId:
|
||||
current.anchorDateKey == today ? '' : current.selectedActionId,
|
||||
habits: habits,
|
||||
actions: actions,
|
||||
sessions: sessions,
|
||||
progressNotes: progressNotes,
|
||||
reflections: reflections,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,21 +3,30 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../core/focus_durations.dart';
|
||||
import '../data/focus_timer_session_store.dart';
|
||||
|
||||
enum FocusTimerStatus { idle, running, paused, completed }
|
||||
|
||||
typedef FocusTimerCompleted =
|
||||
Future<bool> Function(int minutes, int actualSeconds);
|
||||
|
||||
class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
FocusTimerController({
|
||||
required Future<void> Function(int minutes) onCompleted,
|
||||
int initialMinutes = 25,
|
||||
required FocusTimerCompleted onCompleted,
|
||||
FocusTimerSessionStore? sessionStore,
|
||||
int initialMinutes = defaultFocusDurationMinutes,
|
||||
}) : _onCompleted = onCompleted,
|
||||
_sessionStore = sessionStore,
|
||||
_selectedMinutes = initialMinutes,
|
||||
_remainingSeconds = initialMinutes * 60 {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
static const presets = [15, 25, 45];
|
||||
static const presets = focusDurationPresets;
|
||||
|
||||
final Future<void> Function(int minutes) _onCompleted;
|
||||
final FocusTimerCompleted _onCompleted;
|
||||
final FocusTimerSessionStore? _sessionStore;
|
||||
Timer? _ticker;
|
||||
DateTime? _endsAt;
|
||||
int _selectedMinutes;
|
||||
@@ -27,6 +36,8 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
|
||||
int get selectedMinutes => _selectedMinutes;
|
||||
int get remainingSeconds => _remainingSeconds;
|
||||
int get elapsedSeconds =>
|
||||
math.max(0, (_selectedMinutes * 60) - _remainingSeconds);
|
||||
FocusTimerStatus get status => _status;
|
||||
bool get isRunning => _status == FocusTimerStatus.running;
|
||||
bool get isPaused => _status == FocusTimerStatus.paused;
|
||||
@@ -38,6 +49,53 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
: (1 - (_remainingSeconds / totalSeconds)).clamp(0, 1);
|
||||
}
|
||||
|
||||
Future<bool> restoreSavedSession() async {
|
||||
final sessionStore = _sessionStore;
|
||||
if (sessionStore == null) {
|
||||
return false;
|
||||
}
|
||||
final snapshot = await sessionStore.load();
|
||||
if (snapshot == null) {
|
||||
return false;
|
||||
}
|
||||
if (!presets.contains(snapshot.selectedMinutes)) {
|
||||
await sessionStore.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
_selectedMinutes = snapshot.selectedMinutes;
|
||||
final totalSeconds = _selectedMinutes * 60;
|
||||
_remainingSeconds =
|
||||
snapshot.remainingSeconds.clamp(0, totalSeconds).toInt();
|
||||
_ticker?.cancel();
|
||||
_endsAt = null;
|
||||
|
||||
switch (snapshot.status) {
|
||||
case FocusTimerSnapshotStatus.running:
|
||||
_status = FocusTimerStatus.running;
|
||||
_endsAt =
|
||||
snapshot.endsAt ??
|
||||
DateTime.now().add(Duration(seconds: _remainingSeconds));
|
||||
_refreshRemainingTime();
|
||||
if (_remainingSeconds <= 0) {
|
||||
unawaited(_complete());
|
||||
} else {
|
||||
_startTicker();
|
||||
}
|
||||
break;
|
||||
case FocusTimerSnapshotStatus.paused:
|
||||
_status = FocusTimerStatus.paused;
|
||||
break;
|
||||
case FocusTimerSnapshotStatus.completed:
|
||||
_remainingSeconds = 0;
|
||||
_status = FocusTimerStatus.completed;
|
||||
unawaited(_retryCompletedSnapshot(snapshot));
|
||||
break;
|
||||
}
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
void selectMinutes(int minutes) {
|
||||
if (isRunning || !presets.contains(minutes)) {
|
||||
return;
|
||||
@@ -58,6 +116,7 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_status = FocusTimerStatus.running;
|
||||
_endsAt = DateTime.now().add(Duration(seconds: _remainingSeconds));
|
||||
_startTicker();
|
||||
unawaited(_persistActiveSession());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -69,6 +128,7 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_ticker?.cancel();
|
||||
_endsAt = null;
|
||||
_status = FocusTimerStatus.paused;
|
||||
unawaited(_persistActiveSession());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -77,6 +137,7 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_endsAt = null;
|
||||
_remainingSeconds = _selectedMinutes * 60;
|
||||
_status = FocusTimerStatus.idle;
|
||||
unawaited(_clearSavedSession());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -118,19 +179,100 @@ class FocusTimerController extends ChangeNotifier with WidgetsBindingObserver {
|
||||
if (_isCompleting || isCompleted) {
|
||||
return;
|
||||
}
|
||||
if (isRunning) {
|
||||
_refreshRemainingTime();
|
||||
}
|
||||
final totalSeconds = _selectedMinutes * 60;
|
||||
final actualSeconds =
|
||||
_remainingSeconds <= 0
|
||||
? totalSeconds
|
||||
: math.max(1, totalSeconds - _remainingSeconds);
|
||||
_isCompleting = true;
|
||||
_ticker?.cancel();
|
||||
_endsAt = null;
|
||||
_remainingSeconds = 0;
|
||||
_status = FocusTimerStatus.completed;
|
||||
unawaited(_persistCompletedSession(actualSeconds));
|
||||
notifyListeners();
|
||||
try {
|
||||
await _onCompleted(_selectedMinutes);
|
||||
final saved = await _onCompleted(_selectedMinutes, actualSeconds);
|
||||
if (saved) {
|
||||
await _clearSavedSession();
|
||||
}
|
||||
} finally {
|
||||
_isCompleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _retryCompletedSnapshot(
|
||||
FocusTimerSessionSnapshot snapshot,
|
||||
) async {
|
||||
if (_isCompleting || snapshot.actualSeconds == null) {
|
||||
return;
|
||||
}
|
||||
_isCompleting = true;
|
||||
try {
|
||||
final saved = await _onCompleted(
|
||||
snapshot.selectedMinutes,
|
||||
snapshot.actualSeconds!,
|
||||
);
|
||||
if (saved) {
|
||||
await _clearSavedSession();
|
||||
}
|
||||
} finally {
|
||||
_isCompleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistActiveSession() async {
|
||||
final sessionStore = _sessionStore;
|
||||
if (sessionStore == null) {
|
||||
return;
|
||||
}
|
||||
if (isRunning) {
|
||||
await sessionStore.save(
|
||||
FocusTimerSessionSnapshot(
|
||||
status: FocusTimerSnapshotStatus.running,
|
||||
selectedMinutes: _selectedMinutes,
|
||||
remainingSeconds: _remainingSeconds,
|
||||
endsAt: _endsAt,
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isPaused) {
|
||||
await sessionStore.save(
|
||||
FocusTimerSessionSnapshot(
|
||||
status: FocusTimerSnapshotStatus.paused,
|
||||
selectedMinutes: _selectedMinutes,
|
||||
remainingSeconds: _remainingSeconds,
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistCompletedSession(int actualSeconds) async {
|
||||
final sessionStore = _sessionStore;
|
||||
if (sessionStore == null) {
|
||||
return;
|
||||
}
|
||||
await sessionStore.save(
|
||||
FocusTimerSessionSnapshot(
|
||||
status: FocusTimerSnapshotStatus.completed,
|
||||
selectedMinutes: _selectedMinutes,
|
||||
remainingSeconds: 0,
|
||||
actualSeconds: actualSeconds,
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _clearSavedSession() async {
|
||||
await _sessionStore?.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
|
||||
Reference in New Issue
Block a user