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