-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfocus_viewmodel.dart
More file actions
251 lines (218 loc) · 7.6 KB
/
focus_viewmodel.dart
File metadata and controls
251 lines (218 loc) · 7.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../model/note_model.dart';
class FocusViewModel extends ChangeNotifier {
// ==========================================
// 1. STATE FOR NOTES (LOCAL STORAGE & UI)
// ==========================================
final TextEditingController noteController = TextEditingController();
// Temporary variable for the selected image
String? selectedImagePath;
final ImagePicker _picker = ImagePicker();
// List to hold the notes
List<NoteModel> notes = [];
// Constructor: Load saved notes when the ViewModel is initialized
FocusViewModel() {
loadNotesFromDisk();
}
// --- LOCAL STORAGE LOGIC ---
// Save current notes list to device storage
Future<void> saveNotesToDisk() async {
final prefs = await SharedPreferences.getInstance();
List<String> noteStrings = notes.map((n) => n.toJson()).toList();
await prefs.setStringList('saved_notes', noteStrings);
}
// Load saved notes from device storage
Future<void> loadNotesFromDisk() async {
final prefs = await SharedPreferences.getInstance();
List<String>? noteStrings = prefs.getStringList('saved_notes');
if (noteStrings != null) {
List<NoteModel> loadedNotes = [];
for (String noteString in noteStrings) {
try {
loadedNotes.add(NoteModel.fromJson(noteString));
} catch (e) {
debugPrint("Error parsing note JSON: $e");
// Skip the malformed entry and continue
}
}
notes = loadedNotes;
notifyListeners();
}
}
// --- NOTE OPERATIONS ---
// Open gallery to pick an image
Future<void> pickImage() async {
try {
final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
if (image != null) {
selectedImagePath = image.path; // Save local file path
notifyListeners();
}
} catch (e) {
debugPrint("Error picking image: $e");
}
}
// Clear selected image before saving
void removeSelectedImage() {
selectedImagePath = null;
notifyListeners();
}
// Add note (optionally with an image) instantly to the UI and save to disk
Future<void> addNote() async {
final text = noteController.text.trim();
if (text.isEmpty && selectedImagePath == null) return; // Skip if both text and image are empty
notes.insert(0, NoteModel(
id: DateTime.now().millisecondsSinceEpoch.toString(),
content: text,
pinned: false,
imagePath: selectedImagePath, // Store image in model
));
_sortNotes();
await saveNotesToDisk(); // Persist data
noteController.clear();
selectedImagePath = null; // Clear temporary image after saving
notifyListeners();
}
// Remove note instantly and update storage
Future<void> removeNote(String id) async {
notes.removeWhere((note) => note.id == id);
await saveNotesToDisk(); // Persist data
notifyListeners();
}
// Pin/unpin note and update storage
Future<void> togglePin(String id) async {
final index = notes.indexWhere((n) => n.id == id);
if (index != -1) {
notes[index] = NoteModel(
id: notes[index].id,
content: notes[index].content,
pinned: !notes[index].pinned,
imagePath: notes[index].imagePath // Keep image when pinning
);
_sortNotes();
await saveNotesToDisk(); // Persist data
notifyListeners();
}
}
// Sort notes: Pinned items at the top, then by ID (newest first)
void _sortNotes() {
notes.sort((a, b) {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return b.id.compareTo(a.id);
});
}
// ==========================================
// 2. POMODORO TIMER STATE & LOGIC
// ==========================================
int pomodoroTime = 25 * 60;
int shortBreakTime = 5 * 60;
// Hardware settings
bool isVibrationEnabled = true;
int ringtoneType = 1;
// Timer states
bool isPomodoroMode = true;
late int totalTime = pomodoroTime;
late int timeRemaining = pomodoroTime;
bool isRunning = false;
bool isRinging = false; // Flag to check if alarm is currently ringing
Timer? _timer;
// Format time to MM:SS
String get timeString {
String minutes = (timeRemaining ~/ 60).toString().padLeft(2, '0');
String seconds = (timeRemaining % 60).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
// Calculate progress for the circular indicator
double get progress => totalTime <= 0 ? 0.0 : (timeRemaining / totalTime).clamp(0.0, 1.0);
// --- TIMER OPERATIONS ---
// Stop the alarm sound and reset the ringing flag
void stopAlarm() {
FlutterRingtonePlayer().stop();
isRinging = false;
notifyListeners();
}
// Start, pause, or handle alarm state
void toggleTimer() {
// If alarm is ringing, clicking the main button should only stop the alarm
if (isRinging) {
stopAlarm();
return;
}
if (isRunning) {
_timer?.cancel();
isRunning = false;
} else {
isRunning = true;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (timeRemaining > 0) {
timeRemaining--;
} else {
// Time is up
_timer?.cancel();
isRunning = false;
isRinging = true; // Set flag to change UI button state
// Trigger hardware feedback
if (isVibrationEnabled) HapticFeedback.heavyImpact();
if (ringtoneType == 1) FlutterRingtonePlayer().playAlarm();
else if (ringtoneType == 2) FlutterRingtonePlayer().playNotification();
else if (ringtoneType == 3) FlutterRingtonePlayer().playRingtone();
}
notifyListeners();
});
}
notifyListeners();
}
// Reset the current timer back to full duration
void resetTimer() {
stopAlarm(); // Stop alarm if resetting
_timer?.cancel();
isRunning = false;
timeRemaining = totalTime;
notifyListeners();
}
// Switch between Pomodoro and Short Break
void setMode(bool isPomodoro) {
stopAlarm(); // Stop alarm if switching modes
_timer?.cancel();
isRunning = false;
isPomodoroMode = isPomodoro;
totalTime = isPomodoro ? pomodoroTime : shortBreakTime;
timeRemaining = totalTime;
notifyListeners();
}
// Skip current session
void skipTimer() => setMode(!isPomodoroMode);
// Update preferences from the settings dialog
void updateSettings({required int newPomodoroMinutes, required int newBreakMinutes, required bool vibrate, required int ringtone}) {
if (newPomodoroMinutes <= 0 || newBreakMinutes <= 0) {
debugPrint('Lỗi: Thời gian cài đặt phải lớn hơn 0 phút. Đã tự động set về 1.');
newPomodoroMinutes = newPomodoroMinutes <= 0 ? 1 : newPomodoroMinutes;
newBreakMinutes = newBreakMinutes <= 0 ? 1 : newBreakMinutes;
}
stopAlarm(); // Stop alarm if opening settings
pomodoroTime = newPomodoroMinutes * 60;
shortBreakTime = newBreakMinutes * 60;
isVibrationEnabled = vibrate;
ringtoneType = ringtone;
_timer?.cancel();
isRunning = false;
totalTime = isPomodoroMode ? pomodoroTime : shortBreakTime;
timeRemaining = totalTime;
notifyListeners();
}
// Prevent memory leaks when the ViewModel is destroyed
@override
void dispose() {
stopAlarm(); // Ensure alarm doesn't keep ringing in the background
_timer?.cancel();
noteController.dispose();
super.dispose();
}
}