首次远程推送

This commit is contained in:
2026-09-27 23:40:22 +08:00
parent 2e0bf3c784
commit 8afa47952a
70 changed files with 8603 additions and 4502 deletions
+70 -13
View File
@@ -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,
),
);
+147 -5
View File
@@ -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);