diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index e8e00f8..f46e6ba 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -33,7 +33,6 @@
-
createState() => _ForgotPasswordViewState();
-}
-
-class _ForgotPasswordViewState extends State {
- final _vm = ForgotPassViewModel();
-
- @override
- Widget build(BuildContext context) {
- return AnimatedBuilder(
- animation: _vm,
- builder: (context, _) => AuthLayoutTemplate(
- title: 'Quên mật khẩu?',
- subtitle: 'Nhập email của bạn để nhận mã xác thực',
- submitText: 'Gửi mã',
- useCard: false,
- isLoading: _vm.isLoading,
- customHeaderIcon: Container(
- width: 120,
- height: 120,
- decoration: const BoxDecoration(
- color: AppColors.white,
- shape: BoxShape.circle,
- boxShadow: [BoxShadow(color: Color(0xFFD1D9E6), blurRadius: 16)],
- ),
- child: const Icon(
- Icons.lock_reset,
- size: 64,
- color: AppColors.primary,
- ),
- ),
- onSubmit: () async {
- FocusScope.of(context).unfocus();
- final success = await _vm.sendCode();
- if (!context.mounted) return;
-
- if (success) {
- // Jump to Step 2: OTP
- Navigator.push(
- context,
- MaterialPageRoute(builder: (_) => const OtpVerificationView()),
- );
- } else {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Vui lòng nhập email hợp lệ!'),
- backgroundColor: AppColors.error,
- ),
- );
- }
- },
- formContent: CustomTextField(
- label: 'Địa chỉ Email',
- hint: 'example@gmail.com',
- icon: Icons.mail,
- controller: _vm.emailCtrl,
- ),
- footerContent: const Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.help, size: 16, color: AppColors.primary),
- SizedBox(width: 4),
- Text(
- 'Cần hỗ trợ? Liên hệ CSKH',
- style: TextStyle(
- color: AppColors.textSecondary,
- fontSize: 12,
- fontWeight: FontWeight.bold,
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/src/lib/features/auth/login_view.dart b/src/lib/features/auth/login_view.dart
deleted file mode 100644
index a952819..0000000
--- a/src/lib/features/auth/login_view.dart
+++ /dev/null
@@ -1,124 +0,0 @@
-import 'package:flutter/material.dart';
-import '../../core/theme/app_colors.dart';
-import '../../core/theme/auth_layout_template.dart';
-import '../../core/theme/custom_text_field.dart';
-import '../../viewmodels/auth_viewmodels.dart';
-import 'register_view.dart';
-import 'forgot_password_view.dart';
-
-class LoginView extends StatefulWidget {
- const LoginView({super.key});
-
- @override
- State createState() => _LoginViewState();
-}
-
-class _LoginViewState extends State {
- final _vm = LoginViewModel();
-
- @override
- Widget build(BuildContext context) {
- return AnimatedBuilder(
- animation: _vm,
- builder: (context, _) =>
- AuthLayoutTemplate(
- title: 'Task Management',
- subtitle: 'Chào mừng trở lại!',
- submitText: 'Đăng nhập',
- isLoading: _vm.isLoading,
- showSocial: true,
- onSubmit: () async {
- FocusScope.of(context).unfocus(); // Hide keyboard
- final success = await _vm.login();
- if (!context.mounted) return;
-
- if (success) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Đăng nhập thành công!'),
- backgroundColor: AppColors.success,
- ),
- );
- // TODO: Navigator.pushReplacementNamed(context, '/home');
- } else {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Vui lòng điền đầy đủ thông tin!'),
- backgroundColor: AppColors.error,
- ),
- );
- }
- },
- formContent: Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- CustomTextField(
- label: 'Email',
- hint: 'example@gmail.com',
- icon: Icons.mail,
- controller: _vm.emailCtrl,
- ),
- CustomTextField(
- label: 'Mật khẩu',
- hint: '••••••••',
- icon: Icons.lock,
- controller: _vm.passCtrl,
- isPassword: true,
- obscureText: _vm.obscurePass,
- onToggleVisibility: _vm.togglePass,
- ),
- TextButton(
- onPressed: () =>
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (_) => const ForgotPasswordView()),
- ),
- child: const Text(
- 'Quên mật khẩu?',
- style: TextStyle(
- color: AppColors.primary,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ],
- ),
- footerContent: _buildFooter(
- 'Chưa có tài khoản? ', 'Đăng ký ngay', () {
- Navigator.push(
- context,
- MaterialPageRoute(builder: (_) => const RegisterView()),
- );
- }),
- ),
- );
- }
-
- Widget _buildFooter(String text, String action, VoidCallback onTap) {
- return Padding(
- // Position this block 24dp above the bottom of the screen
- padding: const EdgeInsets.only(bottom: 24.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(text, style: const TextStyle(color: Colors.grey, fontSize: 16)),
-
- TextButton(
- onPressed: onTap,
- style: TextButton.styleFrom(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
- minimumSize: const Size(50, 48),
- ),
- child: Text(
- action,
- style: const TextStyle(color: Color(0xFF5A8DF3),
- fontWeight: FontWeight.bold,
- fontSize: 16),
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/src/lib/features/auth/otp_verification_view.dart b/src/lib/features/auth/otp_verification_view.dart
index decae47..1f80d61 100644
--- a/src/lib/features/auth/otp_verification_view.dart
+++ b/src/lib/features/auth/otp_verification_view.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../core/theme/app_colors.dart';
-import '../viewmodels/auth_viewmodels.dart';
-import 'new_password_view.dart';
+import '../auth/viewmodels/auth_viewmodels.dart';
+import '../auth/presentation/view/new_password_view.dart';
class OtpVerificationView extends StatefulWidget {
const OtpVerificationView({super.key});
@@ -12,7 +12,7 @@ class OtpVerificationView extends StatefulWidget {
}
class _OtpVerificationViewState extends State {
- // Bật chế độ 'chờ' cho ViewModel xử lý logic 8 số
+ // Set ViewModel to 'waiting' mode for 8-digit logic processing
final _vm = OtpViewModel();
@override
@@ -27,7 +27,7 @@ class _OtpVerificationViewState extends State {
onPressed: () => Navigator.pop(context),
),
title: const Text(
- 'Xác thực OTP',
+ 'OTP Verification',
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
@@ -40,7 +40,7 @@ class _OtpVerificationViewState extends State {
animation: _vm,
builder: (context, child) {
return SafeArea(
- child: SingleChildScrollView( // Bọc lại đề phòng keyboard hiện lên làm tràn màn hình
+ child: SingleChildScrollView( // Wrap to prevent keyboard overflow
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 40),
child: Column(
@@ -53,7 +53,7 @@ class _OtpVerificationViewState extends State {
),
const SizedBox(height: 32),
const Text(
- 'Nhập mã 8 số', // Hiển thị đúng 8 số
+ 'Enter 8-digit code', // Show correct 8-digit count
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@@ -62,28 +62,28 @@ class _OtpVerificationViewState extends State {
),
const SizedBox(height: 8),
const Text(
- 'Mã đã được gửi đến email của bạn.',
+ 'The code has been sent to your email.',
style: TextStyle(color: AppColors.textSecondary),
),
const SizedBox(height: 40),
- // --- KHU VỰC 8 Ô OTP (ĐÃ SỬA LỖI) ---
- // Dùng LayoutBuilder để tự tính toán kích thước ô cho vừa mọi màn hình
+ // --- 8 OTP BOXES AREA (FIXED) ---
+ // Use LayoutBuilder to calculate box size to fit any screen
LayoutBuilder(
builder: (context, constraints) {
- // Tính toán độ rộng của ô dựa trên màn hình thật, trừ đi khoảng cách giữa các ô
+ // Calculate box width based on actual screen, subtracting space between boxes
double availableWidth = constraints.maxWidth;
- double spaceBetweenBoxes = 6.0; // Khoảng cách giữa các ô
- double totalSpace = spaceBetweenBoxes * 7; // Có 7 khoảng trống giữa 8 ô
- double boxWidth = (availableWidth - totalSpace) / 8; // Độ rộng tối đa mỗi ô
+ double spaceBetweenBoxes = 6.0; // Space between boxes
+ double totalSpace = spaceBetweenBoxes * 7; // 7 gaps between 8 boxes
+ double boxWidth = (availableWidth - totalSpace) / 8; // Max width per box
- // Khống chế độ rộng ô không quá to để nhìn cho art (max 35-40)
+ // Constrain box width to look artistic (max 35-40)
double finalBoxWidth = boxWidth > 38 ? 38 : boxWidth;
return Row(
- mainAxisAlignment: MainAxisAlignment.center, // Căn giữa hàng 8 ô
+ mainAxisAlignment: MainAxisAlignment.center, // Center the row of 8 boxes
children: List.generate(
- 8, // SỬA: Đã tạo đúng 8 ô ở đây
+ 8, // FIX: Created 8 boxes here
(index) => Padding(
padding: EdgeInsets.symmetric(horizontal: spaceBetweenBoxes / 2),
child: _buildOtpBox(index, context, finalBoxWidth),
@@ -94,18 +94,18 @@ class _OtpVerificationViewState extends State {
),
const SizedBox(height: 40),
- // Nút xác nhận xịn sò (Nhận lỗi cụ thể từ Server)
+ // Confirmation button (Handles specific Server errors)
ElevatedButton(
onPressed: _vm.isLoading
? null
: () async {
FocusScope.of(context).unfocus();
- // Gọi hàm verify(), nó trả về String? errorMessage
+ // Call verify(), it returns String? errorMessage
final errorMessage = await _vm.verify();
if (!context.mounted) return;
if (errorMessage == null) {
- // Thành công: Nhảy sang bước 3 (Đổi mật khẩu mới)
+ // Success: Navigate to step 3 (Reset new password)
Navigator.pushReplacement(
context,
MaterialPageRoute(
@@ -113,7 +113,7 @@ class _OtpVerificationViewState extends State {
),
);
} else {
- // Thất bại: Hiện thông báo lỗi cụ thể (ví dụ: "Mã OTP hết hạn")
+ // Failure: Show specific error message (e.g., "OTP code expired")
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
@@ -134,7 +134,7 @@ class _OtpVerificationViewState extends State {
color: AppColors.white,
)
: const Text(
- 'XÁC NHẬN',
+ 'CONFIRM',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -144,7 +144,7 @@ class _OtpVerificationViewState extends State {
),
const SizedBox(height: 16),
- // --- NÚT GỬI LẠI MÃ (CHỈ CÓ Ở BẢN XỊN) ---
+ // --- RESEND CODE BUTTON ---
TextButton.icon(
onPressed: _vm.isLoading
? null
@@ -155,7 +155,7 @@ class _OtpVerificationViewState extends State {
if (errorMessage == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
- content: Text('Đã gửi lại mã OTP!'),
+ content: Text('OTP code resent!'),
backgroundColor: AppColors.success,
),
);
@@ -170,7 +170,7 @@ class _OtpVerificationViewState extends State {
},
icon: const Icon(Icons.refresh, size: 18, color: AppColors.primary),
label: const Text(
- 'Gửi lại mã',
+ 'Resend code',
style: TextStyle(color: AppColors.primary, fontWeight: FontWeight.bold),
),
)
@@ -184,10 +184,10 @@ class _OtpVerificationViewState extends State {
);
}
- // SỬA: Nhận thêm 'boxWidth' để tự động co dãn cho vừa 8 ô
+ // FIX: Added 'boxWidth' to automatically scale for 8 boxes
Widget _buildOtpBox(int index, BuildContext context, double boxWidth) {
return Container(
- width: boxWidth, // Sử dụng độ rộng đã tính toán
+ width: boxWidth, // Use calculated width
height: 48,
decoration: BoxDecoration(
color: AppColors.white,
@@ -197,7 +197,7 @@ class _OtpVerificationViewState extends State {
child: TextField(
onChanged: (value) {
_vm.updateDigit(index, value);
- // SỬA: Logic nhảy focus chuẩn cho 8 ô (index chạy từ 0 đến 7)
+ // FIX: Correct focus jump logic for 8 boxes (index from 0 to 7)
if (value.isNotEmpty && index < 7) {
FocusScope.of(context).nextFocus();
}
@@ -223,4 +223,4 @@ class _OtpVerificationViewState extends State {
),
);
}
-}
\ No newline at end of file
+}
diff --git a/src/lib/features/auth/presentation/view/new_password_view.dart b/src/lib/features/auth/presentation/view/new_password_view.dart
index 575b837..dbe9bfc 100644
--- a/src/lib/features/auth/presentation/view/new_password_view.dart
+++ b/src/lib/features/auth/presentation/view/new_password_view.dart
@@ -7,7 +7,6 @@ import '../viewmodels/auth_viewmodels.dart';
class NewPasswordView extends StatefulWidget {
const NewPasswordView({super.key});
-
@override
State createState() => _NewPasswordViewState();
}
@@ -78,4 +77,4 @@ class _NewPasswordViewState extends State {
),
);
}
-}
+}
\ No newline at end of file
diff --git a/src/lib/features/auth/presentation/viewmodels/auth_viewmodels.dart b/src/lib/features/auth/presentation/viewmodels/auth_viewmodels.dart
index 4af1ee1..5808490 100644
--- a/src/lib/features/auth/presentation/viewmodels/auth_viewmodels.dart
+++ b/src/lib/features/auth/presentation/viewmodels/auth_viewmodels.dart
@@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
-import '../../data/auth_helper.dart';
+import '../../services/auth_helper.dart';
// ==========================================
// BASE VIEWMODEL (Handles Loading, Validation & Errors)
@@ -123,6 +123,12 @@ class LoginViewModel extends BaseViewModel {
setLoading(false);
}
}
+ @override
+ void dispose() {
+ emailCtrl.dispose();
+ passCtrl.dispose();
+ super.dispose();
+ }
}
@@ -172,6 +178,14 @@ class RegisterViewModel extends BaseViewModel {
setLoading(false);
}
}
+ @override
+ void dispose() {
+ usernameCtrl.dispose();
+ emailCtrl.dispose();
+ passCtrl.dispose();
+ confirmPassCtrl.dispose();
+ super.dispose();
+ }
}
// ==========================================
@@ -201,6 +215,11 @@ class ForgotPassViewModel extends BaseViewModel {
setLoading(false);
}
}
+ @override
+ void dispose() {
+ emailCtrl.dispose();
+ super.dispose();
+ }
}
// ==========================================
@@ -287,4 +306,10 @@ class NewPassViewModel extends BaseViewModel {
setLoading(false);
}
}
+ @override
+ void dispose() {
+ passCtrl.dispose();
+ confirmPassCtrl.dispose();
+ super.dispose();
+ }
}
diff --git a/src/lib/features/auth/register_view.dart b/src/lib/features/auth/register_view.dart
deleted file mode 100644
index 5234f35..0000000
--- a/src/lib/features/auth/register_view.dart
+++ /dev/null
@@ -1,105 +0,0 @@
-import 'package:flutter/material.dart';
-import '../../core/theme/app_colors.dart';
-import '../../core/theme/auth_layout_template.dart';
-import '../../core/theme/custom_text_field.dart';
-import '../../viewmodels/auth_viewmodels.dart';
-
-class RegisterView extends StatefulWidget {
- const RegisterView({super.key});
-
- @override
- State createState() => _RegisterViewState();
-}
-
-class _RegisterViewState extends State {
- final _vm = RegisterViewModel();
-
- @override
- Widget build(BuildContext context) {
- return AnimatedBuilder(
- animation: _vm,
- builder: (context, _) => AuthLayoutTemplate(
- title: 'Tạo tài khoản mới',
- subtitle: 'Bắt đầu quản lý công việc khoa học',
- submitText: 'Đăng ký',
- isLoading: _vm.isLoading,
- showSocial: true,
- onSubmit: () async {
- FocusScope.of(context).unfocus();
- final success = await _vm.register();
- if (!context.mounted) return;
-
- if (success) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Đăng ký thành công!'),
- backgroundColor: AppColors.success,
- ),
- );
- Navigator.pop(context); // Go back to login
- } else {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Mật khẩu không khớp hoặc thiếu thông tin!'),
- backgroundColor: AppColors.error,
- ),
- );
- }
- },
- formContent: Column(
- children: [
- CustomTextField(
- label: 'Họ tên',
- hint: 'Nhập họ và tên',
- icon: Icons.person,
- controller: _vm.nameCtrl,
- ),
- CustomTextField(
- label: 'Email',
- hint: 'example@gmail.com',
- icon: Icons.mail,
- controller: _vm.emailCtrl,
- ),
- CustomTextField(
- label: 'Mật khẩu',
- hint: '••••••••',
- icon: Icons.lock,
- controller: _vm.passCtrl,
- isPassword: true,
- obscureText: _vm.obscurePass,
- onToggleVisibility: _vm.togglePass,
- ),
- CustomTextField(
- label: 'Xác nhận mật khẩu',
- hint: '••••••••',
- icon: Icons.shield,
- controller: _vm.confirmPassCtrl,
- isPassword: true,
- obscureText: _vm.obscurePass,
- onToggleVisibility: _vm.togglePass,
- ),
- ],
- ),
- footerContent: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Text(
- 'Đã có tài khoản? ',
- style: TextStyle(color: AppColors.textSecondary),
- ),
- GestureDetector(
- onTap: () => Navigator.pop(context),
- child: const Text(
- 'Đăng nhập',
- style: TextStyle(
- color: AppColors.primary,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/src/lib/features/auth/data/auth_helper.dart b/src/lib/features/auth/services/auth_helper.dart
similarity index 99%
rename from src/lib/features/auth/data/auth_helper.dart
rename to src/lib/features/auth/services/auth_helper.dart
index 71f7594..19ba933 100644
--- a/src/lib/features/auth/data/auth_helper.dart
+++ b/src/lib/features/auth/services/auth_helper.dart
@@ -1,4 +1,4 @@
-// data/helpers/auth_helper.dart
+// services/helpers/auth_helper.dart
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
diff --git a/src/lib/features/auth/viewmodels/auth_viewmodels.dart b/src/lib/features/auth/viewmodels/auth_viewmodels.dart
index 9d97852..68a1a47 100644
--- a/src/lib/features/auth/viewmodels/auth_viewmodels.dart
+++ b/src/lib/features/auth/viewmodels/auth_viewmodels.dart
@@ -18,13 +18,13 @@ class LoginViewModel extends BaseViewModel {
void togglePass() { obscurePass = !obscurePass; notifyListeners(); }
- Future login() async {
- if (emailCtrl.text.isEmpty || passCtrl.text.isEmpty) return false;
+ Future login() async {
+ if (emailCtrl.text.isEmpty || passCtrl.text.isEmpty) return "Please enter all fields";
setLoading(true);
// Mock API call
- await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
+ await Future.delayed(const Duration(seconds: 1));
setLoading(false);
- return true; // Assume success
+ return null; // Success
}
}
@@ -37,38 +37,53 @@ class RegisterViewModel extends BaseViewModel {
void togglePass() { obscurePass = !obscurePass; notifyListeners(); }
- Future register() async {
- if (passCtrl.text != confirmPassCtrl.text) return false;
+ Future register() async {
+ if (passCtrl.text != confirmPassCtrl.text) return "Passwords do not match";
setLoading(true);
- await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
+ await Future.delayed(const Duration(seconds: 1));
setLoading(false);
- return true;
+ return null;
}
}
class ForgotPassViewModel extends BaseViewModel {
final emailCtrl = TextEditingController();
- Future sendCode() async {
- if (emailCtrl.text.isEmpty) return false;
+ Future sendCode() async {
+ if (emailCtrl.text.isEmpty) return "Please enter email";
setLoading(true);
- await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
+ await Future.delayed(const Duration(seconds: 1));
setLoading(false);
- return true;
+ return null;
}
}
class OtpViewModel extends BaseViewModel {
- List digits = List.filled(6, "");
+ // Updated to 8 digits to match UI
+ List digits = List.filled(8, "");
+
+ void updateDigit(int idx, String val) {
+ if (idx < digits.length) {
+ digits[idx] = val;
+ notifyListeners();
+ }
+ }
- void updateDigit(int idx, String val) { digits[idx] = val; notifyListeners(); }
+ Future verify() async {
+ String code = digits.join();
+ if (code.length < 8) return "Please enter 8-digit OTP";
+ setLoading(true);
+ await Future.delayed(const Duration(seconds: 1));
+ setLoading(false);
+ return null; // Success
+ }
- Future verify() async {
- if (digits.join().length < 6) return false;
+ // Added resend method to fix error in UI
+ Future resend() async {
setLoading(true);
- await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
+ await Future.delayed(const Duration(seconds: 1));
setLoading(false);
- return true;
+ return null; // Success
}
}
@@ -79,11 +94,11 @@ class NewPassViewModel extends BaseViewModel {
void togglePass() { obscurePass = !obscurePass; notifyListeners(); }
- Future updatePassword() async {
- if (passCtrl.text != confirmPassCtrl.text) return false;
+ Future updatePassword() async {
+ if (passCtrl.text != confirmPassCtrl.text) return "Passwords do not match";
setLoading(true);
- await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
+ await Future.delayed(const Duration(seconds: 1));
setLoading(false);
- return true;
+ return null;
}
-}
\ No newline at end of file
+}
diff --git a/src/lib/features/main/view/screens/main_screen.dart b/src/lib/features/main/view/screens/main_screen.dart
index e6f3b16..89a477a 100644
--- a/src/lib/features/main/view/screens/main_screen.dart
+++ b/src/lib/features/main/view/screens/main_screen.dart
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:task_management_app/features/statistics/viewmodel/statistics_viewmodel.dart';
import 'package:task_management_app/features/tasks/view/screens/home_screen.dart';
import '../../../../core/theme/app_colors.dart';
+import '../../../note/view/focus_screen.dart';
+import '../../../note/viewmodel/focus_viewmodel.dart';
import '../../../statistics/view/screens/statistics_screen.dart';
// import '../../../tasks/view/screens/home_screen.dart';
import 'package:provider/provider.dart';
@@ -19,7 +21,10 @@ class _MainScreenState extends State {
final List _screens = [
const Center(child: HomeScreen()),
const Center(child: Text('Màn hình Lịch')),
- const Center(child: Text('Màn hình Tập trung')),
+ ChangeNotifierProvider(
+ create: (_) => FocusViewModel(),
+ child: const FocusScreen(),
+ ),
ChangeNotifierProvider(
create: (_) => StatisticsViewmodel(),
child: const StatisticsScreen(),
diff --git a/src/lib/features/note/model/note_model.dart b/src/lib/features/note/model/note_model.dart
new file mode 100644
index 0000000..c0c0a02
--- /dev/null
+++ b/src/lib/features/note/model/note_model.dart
@@ -0,0 +1,41 @@
+import 'dart:convert';
+
+class NoteModel {
+ final String id;
+ final String content;
+ final bool pinned;
+ final String? imagePath;
+
+ NoteModel({
+ required this.id,
+ required this.content,
+ this.pinned = false,
+ this.imagePath,
+ });
+
+ // --- LOCAL STORAGE HELPERS ---
+
+ // Convert Object to Map for local storage
+ Map toMap() {
+ return {
+ 'id': id,
+ 'content': content,
+ 'pinned': pinned,
+ 'imagePath': imagePath,
+ };
+ }
+
+ // Create Object from Map
+ factory NoteModel.fromMap(Map map) {
+ return NoteModel(
+ id: map['id'],
+ content: map['content'],
+ pinned: map['pinned'] ?? false,
+ imagePath: map['imagePath'],
+ );
+ }
+
+ // JSON Helpers for SharedPreferences
+ String toJson() => json.encode(toMap());
+ factory NoteModel.fromJson(String source) => NoteModel.fromMap(json.decode(source));
+}
\ No newline at end of file
diff --git a/src/lib/features/note/view/focus_screen.dart b/src/lib/features/note/view/focus_screen.dart
new file mode 100644
index 0000000..9349b49
--- /dev/null
+++ b/src/lib/features/note/view/focus_screen.dart
@@ -0,0 +1,147 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../../../../core/theme/app_colors.dart';
+import '../viewmodel/focus_viewmodel.dart';
+import '../view/focus_widget.dart';
+
+class FocusScreen extends StatelessWidget {
+ const FocusScreen({super.key});
+
+ // Shows the configuration dialog for timer and notification settings
+ void _showSettingsDialog(BuildContext context) {
+ final vm = context.read();
+
+ double currentPomodoro = (vm.pomodoroTime / 60).toDouble();
+ double currentBreak = (vm.shortBreakTime / 60).toDouble();
+ bool currentVibrate = vm.isVibrationEnabled;
+ int currentRingtone = vm.ringtoneType;
+
+ showDialog(
+ context: context,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (context, setStateDialog) {
+ return AlertDialog(
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+ title: const Text('Cài đặt', style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // --- Time Settings ---
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('Pomodoro', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
+ Text('${currentPomodoro.toInt()} phút', style: const TextStyle(color: AppColors.primaryBlue, fontWeight: FontWeight.bold)),
+ ],
+ ),
+ Slider(value: currentPomodoro, min: 5, max: 60, divisions: 55, activeColor: AppColors.primaryBlue, onChanged: (val) => setStateDialog(() => currentPomodoro = val)),
+ const SizedBox(height: 5),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('Nghỉ ngắn', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
+ Text('${currentBreak.toInt()} phút', style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold)),
+ ],
+ ),
+ Slider(value: currentBreak, min: 1, max: 30, divisions: 29, activeColor: Colors.orange, onChanged: (val) => setStateDialog(() => currentBreak = val)),
+ const Divider(height: 30),
+
+ // --- Hardware Settings ---
+ SwitchListTile(
+ title: const Text('Rung', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
+ value: currentVibrate, activeColor: AppColors.primaryBlue, contentPadding: EdgeInsets.zero,
+ onChanged: (val) => setStateDialog(() => currentVibrate = val),
+ ),
+ const SizedBox(height: 10),
+ const Text('Âm thanh', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
+ const SizedBox(height: 10),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 15),
+ decoration: BoxDecoration(color: const Color(0xFFF4F6F9), borderRadius: BorderRadius.circular(15)),
+ child: DropdownButtonHideUnderline(
+ child: DropdownButton(
+ isExpanded: true,
+ value: currentRingtone,
+ items: const [
+ DropdownMenuItem(value: 1, child: Text('Chuông (Lớn)')),
+ DropdownMenuItem(value: 2, child: Text('Thông báo (Nhỏ)')),
+ DropdownMenuItem(value: 3, child: Text('Nhạc chuông')),
+ ],
+ onChanged: (val) {
+ if (val != null) setStateDialog(() => currentRingtone = val);
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Hủy', style: TextStyle(color: Colors.grey))),
+ ElevatedButton(
+ onPressed: () {
+ // Update settings in ViewModel
+ vm.updateSettings(newPomodoroMinutes: currentPomodoro.toInt(), newBreakMinutes: currentBreak.toInt(), vibrate: currentVibrate, ringtone: currentRingtone);
+ Navigator.pop(context);
+ },
+ style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
+ child: const Text('Lưu', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ );
+ },
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFF4F6F9),
+ body: SafeArea(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ // --- Header ---
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Row(
+ children: [
+ CircleAvatar(radius: 22, backgroundImage: NetworkImage('https://i.pravatar.cc/150?u=a042581f4e29026704d')),
+ SizedBox(width: 15),
+ Text('Tập trung', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))),
+ ],
+ ),
+ // Settings Icon
+ GestureDetector(
+ onTap: () => _showSettingsDialog(context),
+ child: Container(padding: const EdgeInsets.all(10), decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: const Icon(Icons.settings_outlined, color: AppColors.primaryBlue)),
+ ),
+ ],
+ ),
+ const SizedBox(height: 30),
+
+ // --- Main Content Widgets ---
+ const FocusTabSelector(),
+ const SizedBox(height: 30),
+ const TimerDisplayWidget(),
+ const SizedBox(height: 40),
+ const TimerControlsWidget(),
+ const SizedBox(height: 40),
+ const QuickNoteCard(),
+ const SizedBox(height: 80), // Padding for bottom nav bar
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/lib/features/note/view/focus_widget.dart b/src/lib/features/note/view/focus_widget.dart
new file mode 100644
index 0000000..8037b4a
--- /dev/null
+++ b/src/lib/features/note/view/focus_widget.dart
@@ -0,0 +1,409 @@
+import 'dart:io'; // Import to display image files
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../../../../core/theme/app_colors.dart';
+import '../viewmodel/focus_viewmodel.dart';
+
+// --- Tab Selector Widget ---
+class FocusTabSelector extends StatelessWidget {
+ const FocusTabSelector({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final vm = context.watch();
+
+ return Container(
+ padding: const EdgeInsets.all(5),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 10)],
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildTabBtn('Pomodoro', vm.isPomodoroMode, () => vm.setMode(true)),
+ _buildTabBtn('Nghỉ ngắn', !vm.isPomodoroMode, () => vm.setMode(false)),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTabBtn(String label, bool isSelected, VoidCallback onTap) {
+ return GestureDetector(
+ onTap: onTap,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
+ decoration: BoxDecoration(
+ color: isSelected ? AppColors.primaryBlue : Colors.transparent,
+ borderRadius: BorderRadius.circular(15),
+ ),
+ child: Text(
+ label,
+ style: TextStyle(
+ color: isSelected ? Colors.white : const Color(0xFF757575),
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// --- Timer Display Widget ---
+class TimerDisplayWidget extends StatelessWidget {
+ const TimerDisplayWidget({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final vm = context.watch();
+ return Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ width: 280,
+ height: 280,
+ decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 14)),
+ ),
+ SizedBox(
+ width: 280,
+ height: 280,
+ child: CircularProgressIndicator(
+ value: vm.progress,
+ strokeWidth: 14,
+ color: AppColors.primaryBlue,
+ backgroundColor: Colors.transparent,
+ strokeCap: StrokeCap.round,
+ ),
+ ),
+ Container(
+ width: 210,
+ height: 210,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ shape: BoxShape.circle,
+ boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 20, offset: const Offset(0, 10))],
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ vm.timeString,
+ style: const TextStyle(fontSize: 56, fontWeight: FontWeight.w900, color: Color(0xFF2C3E50), letterSpacing: -2),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ vm.isPomodoroMode ? 'TẬP TRUNG' : 'NGHỈ NGƠI',
+ style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: AppColors.primaryBlue.withOpacity(0.8), letterSpacing: 2),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+// --- Timer Controls Widget ---
+class TimerControlsWidget extends StatelessWidget {
+ const TimerControlsWidget({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final vm = context.watch();
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ _buildControlBtn(
+ icon: Icons.replay_rounded,
+ bgColor: Colors.white,
+ iconColor: const Color(0xFF757575),
+ size: 60,
+ onTap: vm.resetTimer,
+ ),
+ const SizedBox(width: 30),
+
+ // MAIN BUTTON: CHANGES TO RED WHEN RINGING TO STOP ALARM
+ _buildControlBtn(
+ icon: vm.isRinging
+ ? Icons.notifications_off_rounded // Muted bell icon
+ : (vm.isRunning ? Icons.pause_rounded : Icons.play_arrow_rounded),
+ bgColor: vm.isRinging ? Colors.redAccent : AppColors.primaryBlue,
+ iconColor: Colors.white,
+ size: 85,
+ hasShadow: true,
+ onTap: vm.toggleTimer, // Stops alarm if ringing, otherwise toggles timer
+ ),
+
+ const SizedBox(width: 30),
+ _buildControlBtn(
+ icon: Icons.skip_next_rounded,
+ bgColor: Colors.white,
+ iconColor: const Color(0xFF757575),
+ size: 60,
+ onTap: vm.skipTimer,
+ ),
+ ],
+ );
+ }
+
+ Widget _buildControlBtn({
+ required IconData icon,
+ required Color bgColor,
+ required Color iconColor,
+ required double size,
+ bool hasShadow = false,
+ required VoidCallback onTap,
+ }) {
+ return Container(
+ width: size,
+ height: size,
+ decoration: BoxDecoration(
+ color: bgColor,
+ shape: BoxShape.circle,
+ boxShadow: [
+ if (hasShadow) BoxShadow(color: bgColor.withOpacity(0.4), blurRadius: 20, offset: const Offset(0, 10)),
+ if (!hasShadow) BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 5)),
+ ],
+ ),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ borderRadius: BorderRadius.circular(100),
+ onTap: onTap,
+ child: Icon(icon, color: iconColor, size: size * 0.45),
+ ),
+ ),
+ );
+ }
+}
+
+// --- Quick Note Card Widget ---
+class QuickNoteCard extends StatelessWidget {
+ const QuickNoteCard({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final vm = context.watch();
+
+ return Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(25),
+ decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(30)),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: const [
+ Text('Ghi chú nhanh', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))),
+ Text('Đang thực hiện', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.grayText)),
+ ],
+ ),
+ const SizedBox(height: 15),
+
+ // TEXT INPUT AND IMAGE PREVIEW AREA
+ Container(
+ padding: const EdgeInsets.all(15),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF4F6F9),
+ borderRadius: BorderRadius.circular(15),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ TextField(
+ controller: vm.noteController,
+ maxLines: 3,
+ decoration: InputDecoration(
+ hintText: 'Thêm ý tưởng, tiến độ, hình ảnh...',
+ hintStyle: TextStyle(color: Colors.grey.shade400, fontSize: 14),
+ border: InputBorder.none,
+ isDense: true,
+ contentPadding: EdgeInsets.zero,
+ ),
+ style: const TextStyle(fontSize: 14, color: Color(0xFF2C3E50)),
+ ),
+
+ // SHOW IMAGE PREVIEW IF SELECTED
+ if (vm.selectedImagePath != null) ...[
+ const SizedBox(height: 10),
+ Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image.file(
+ File(vm.selectedImagePath!),
+ height: 80,
+ width: 80,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ height: 80,
+ width: 80,
+ decoration: BoxDecoration(
+ color: Colors.grey.shade300,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Icon(
+ Icons.broken_image_outlined,
+ color: Colors.grey.shade600,
+ size: 40,
+ ),
+ );
+ },
+ ),
+ ),
+ Positioned(
+ top: -10,
+ right: -10,
+ child: IconButton(
+ icon: const Icon(Icons.cancel, color: Colors.redAccent),
+ onPressed: vm.removeSelectedImage,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ],
+ ),
+ ),
+ const SizedBox(height: 15),
+
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ Icon(Icons.attach_file_rounded, color: Colors.grey.shade400, size: 22),
+ const SizedBox(width: 10),
+ // IMAGE PICKER BUTTON
+ GestureDetector(
+ onTap: () => vm.pickImage(),
+ child: Icon(Icons.image_outlined, color: AppColors.primaryBlue, size: 22),
+ ),
+ ],
+ ),
+ ElevatedButton(
+ onPressed: () {
+ FocusScope.of(context).unfocus(); // Dismiss keyboard
+ context.read().addNote();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.primaryBlue,
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
+ elevation: 0,
+ ),
+ child: const Text('Lưu ghi chú', style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)),
+ ),
+ ],
+ ),
+
+ // --- LOCAL NOTE LIST WITH IMAGE SUPPORT ---
+ if (vm.notes.isNotEmpty) ...[
+ const Padding(padding: EdgeInsets.symmetric(vertical: 15), child: Divider(color: Color(0xFFE2E8F0), height: 1)),
+ ListView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: vm.notes.length,
+ itemBuilder: (context, index) {
+ final note = vm.notes[index];
+ return Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: note.pinned ? const Color(0xFFFFF8E1) : const Color(0xFFF8FAFC),
+ borderRadius: BorderRadius.circular(15),
+ border: Border.all(color: note.pinned ? Colors.amber.shade200 : Colors.transparent),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // CLICK TO REMOVE BUTTON
+ GestureDetector(
+ onTap: () => vm.removeNote(note.id),
+ child: Container(
+ margin: const EdgeInsets.only(top: 2, right: 12),
+ width: 22,
+ height: 22,
+ decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: AppColors.primaryBlue, width: 2)),
+ ),
+ ),
+ // CONTENT (TEXT AND IMAGE)
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (note.content.isNotEmpty)
+ Text(note.content, style: const TextStyle(fontSize: 14, color: Color(0xFF2C3E50), height: 1.4)),
+
+ // DISPLAY ATTACHED IMAGE IN NOTE
+ if (note.imagePath != null && note.imagePath!.isNotEmpty) ...[
+ const SizedBox(height: 8),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ child: Image.file(
+ File(note.imagePath!),
+ width: double.infinity,
+ height: 150,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ width: double.infinity,
+ height: 150,
+ decoration: BoxDecoration(
+ color: Colors.grey.shade300,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.broken_image_outlined,
+ color: Colors.grey.shade600,
+ size: 50,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Image not available',
+ style: TextStyle(
+ color: Colors.grey.shade600,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ // PIN BUTTON
+ GestureDetector(
+ onTap: () => vm.togglePin(note.id),
+ child: Padding(
+ padding: const EdgeInsets.only(left: 8),
+ child: Icon(
+ note.pinned ? Icons.push_pin_rounded : Icons.push_pin_outlined,
+ color: note.pinned ? Colors.orange : Colors.grey.shade400,
+ size: 20,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/lib/features/note/viewmodel/focus_viewmodel.dart b/src/lib/features/note/viewmodel/focus_viewmodel.dart
new file mode 100644
index 0000000..50f0e5d
--- /dev/null
+++ b/src/lib/features/note/viewmodel/focus_viewmodel.dart
@@ -0,0 +1,246 @@
+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 notes = [];
+
+ // Constructor: Load saved notes when the ViewModel is initialized
+ FocusViewModel() {
+ loadNotesFromDisk();
+ }
+
+ // --- LOCAL STORAGE LOGIC ---
+
+ // Save current notes list to device storage
+ Future saveNotesToDisk() async {
+ final prefs = await SharedPreferences.getInstance();
+ List noteStrings = notes.map((n) => n.toJson()).toList();
+ await prefs.setStringList('saved_notes', noteStrings);
+ }
+
+ // Load saved notes from device storage
+ Future loadNotesFromDisk() async {
+ final prefs = await SharedPreferences.getInstance();
+ List? noteStrings = prefs.getStringList('saved_notes');
+ if (noteStrings != null) {
+ List 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 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 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 removeNote(String id) async {
+ notes.removeWhere((note) => note.id == id);
+ await saveNotesToDisk(); // Persist data
+ notifyListeners();
+ }
+
+ // Pin/unpin note and update storage
+ Future 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 => timeRemaining / totalTime;
+
+ // --- 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}) {
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/src/lib/features/tasks/view/screens/task_detail_screen.dart b/src/lib/features/tasks/view/screens/task_detail_screen.dart
index 6afd045..6297293 100644
--- a/src/lib/features/tasks/view/screens/task_detail_screen.dart
+++ b/src/lib/features/tasks/view/screens/task_detail_screen.dart
@@ -24,7 +24,7 @@ class _TaskDetailScreenState extends State {
@override
void initState() {
super.initState();
- // Initialize state variables with data from the passed task object
+ // Initialize state variables with services from the passed task object
_titleController = TextEditingController(text: widget.task.title);
_descController = TextEditingController(text: widget.task.description);
_startTime = widget.task.startTime;
diff --git a/src/pubspec.lock b/src/pubspec.lock
index 8cfed75..efabe5b 100644
--- a/src/pubspec.lock
+++ b/src/pubspec.lock
@@ -53,10 +53,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+ sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
- version: "1.4.1"
+ version: "1.4.0"
clock:
dependency: transitive
description:
@@ -89,6 +89,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
+ cross_file:
+ dependency: transitive
+ description:
+ name: cross_file
+ sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.5+2"
crypto:
dependency: transitive
description:
@@ -145,6 +153,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
+ file_selector_linux:
+ dependency: transitive
+ description:
+ name: file_selector_linux
+ sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.4"
+ file_selector_macos:
+ dependency: transitive
+ description:
+ name: file_selector_macos
+ sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.5"
+ file_selector_platform_interface:
+ dependency: transitive
+ description:
+ name: file_selector_platform_interface
+ sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
+ file_selector_windows:
+ dependency: transitive
+ description:
+ name: file_selector_windows
+ sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.3+5"
flutter:
dependency: "direct main"
description: flutter
@@ -166,6 +206,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.34"
+ flutter_ringtone_player:
+ dependency: "direct main"
+ description:
+ name: flutter_ringtone_player
+ sha256: ae4a2caab2cfd14902f3ee5fe0307c454b396f0077fa34700238d6c4bd99cfec
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0+4"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -240,6 +296,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
+ image_picker:
+ dependency: "direct main"
+ description:
+ name: image_picker
+ sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ image_picker_android:
+ dependency: transitive
+ description:
+ name: image_picker_android
+ sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.13+16"
+ image_picker_for_web:
+ dependency: transitive
+ description:
+ name: image_picker_for_web
+ sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ image_picker_ios:
+ dependency: transitive
+ description:
+ name: image_picker_ios
+ sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.13+6"
+ image_picker_linux:
+ dependency: transitive
+ description:
+ name: image_picker_linux
+ sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
+ image_picker_macos:
+ dependency: transitive
+ description:
+ name: image_picker_macos
+ sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2+1"
+ image_picker_platform_interface:
+ dependency: transitive
+ description:
+ name: image_picker_platform_interface
+ sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.11.1"
+ image_picker_windows:
+ dependency: transitive
+ description:
+ name: image_picker_windows
+ sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
intl:
dependency: "direct main"
description:
@@ -300,18 +420,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
- version: "0.12.19"
+ version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
- version: "0.13.0"
+ version: "0.11.1"
meta:
dependency: transitive
description:
@@ -441,7 +561,7 @@ packages:
source: hosted
version: "2.6.0"
provider:
- dependency: transitive
+ dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
@@ -609,10 +729,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
- version: "0.7.10"
+ version: "0.7.7"
typed_data:
dependency: transitive
description:
diff --git a/src/pubspec.yaml b/src/pubspec.yaml
index e4310e1..65c8e9c 100644
--- a/src/pubspec.yaml
+++ b/src/pubspec.yaml
@@ -39,6 +39,9 @@ dependencies:
supabase_flutter: ^2.12.2
flutter_timezone: ^5.0.2
provider: ^6.1.5+1
+ flutter_ringtone_player: ^4.0.0+4
+ image_picker: ^1.2.1
+ shared_preferences: ^2.2.2
dev_dependencies:
flutter_test:
@@ -88,4 +91,4 @@ flutter:
# weight: 700
#
# For details regarding fonts from package dependencies,
- # see https://flutter.dev/to/font-from-package
+ # see https://flutter.dev/to/font-from-package
\ No newline at end of file