Skip to content

Commit f96dcc6

Browse files
committed
feat: implement full Focus feature set
- Added Pomodoro timer with Start/Reset/Skip logic. - Integrated local Quick Notes with Pin/Delete functionality. - Supported image attachments in notes using image_picker. - Added Focus settings: time duration, vibration, and ringtones.
1 parent 949d75a commit f96dcc6

13 files changed

Lines changed: 890 additions & 65 deletions

File tree

src/android/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
<category android:name="android.intent.category.LAUNCHER"/>
3434
</intent-filter>
3535
</activity>
36-
<!-- Don't delete the meta-data below.
3736
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
3837
<meta-data
3938
android:name="flutterEmbedding"

src/lib/features/auth/otp_verification_view.dart

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter/services.dart';
33
import '../../core/theme/app_colors.dart';
4-
import '../viewmodels/auth_viewmodels.dart';
5-
import 'new_password_view.dart';
4+
import '../auth/viewmodels/auth_viewmodels.dart';
5+
import '../auth/presentation/view/new_password_view.dart';
66

77
class OtpVerificationView extends StatefulWidget {
88
const OtpVerificationView({super.key});
@@ -12,7 +12,7 @@ class OtpVerificationView extends StatefulWidget {
1212
}
1313

1414
class _OtpVerificationViewState extends State<OtpVerificationView> {
15-
// Bật chế độ 'chờ' cho ViewModel xử lý logic 8 số
15+
// Set ViewModel to 'waiting' mode for 8-digit logic processing
1616
final _vm = OtpViewModel();
1717

1818
@override
@@ -27,7 +27,7 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
2727
onPressed: () => Navigator.pop(context),
2828
),
2929
title: const Text(
30-
'Xác thực OTP',
30+
'OTP Verification',
3131
style: TextStyle(
3232
color: AppColors.primary,
3333
fontWeight: FontWeight.bold,
@@ -40,7 +40,7 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
4040
animation: _vm,
4141
builder: (context, child) {
4242
return SafeArea(
43-
child: SingleChildScrollView( // Bọc lại đề phòng keyboard hiện lên làm tràn màn hình
43+
child: SingleChildScrollView( // Wrap to prevent keyboard overflow
4444
child: Padding(
4545
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 40),
4646
child: Column(
@@ -53,7 +53,7 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
5353
),
5454
const SizedBox(height: 32),
5555
const Text(
56-
'Nhập mã 8 số', // Hiển thị đúng 8 số
56+
'Enter 8-digit code', // Show correct 8-digit count
5757
style: TextStyle(
5858
fontSize: 24,
5959
fontWeight: FontWeight.bold,
@@ -62,28 +62,28 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
6262
),
6363
const SizedBox(height: 8),
6464
const Text(
65-
'Mã đã được gửi đến email của bạn.',
65+
'The code has been sent to your email.',
6666
style: TextStyle(color: AppColors.textSecondary),
6767
),
6868
const SizedBox(height: 40),
6969

70-
// --- KHU VỰC 8 Ô OTP (ĐÃ SỬA LỖI) ---
71-
// Dùng LayoutBuilder để tự tính toán kích thước ô cho vừa mọi màn hình
70+
// --- 8 OTP BOXES AREA (FIXED) ---
71+
// Use LayoutBuilder to calculate box size to fit any screen
7272
LayoutBuilder(
7373
builder: (context, constraints) {
74-
// 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 ô
74+
// Calculate box width based on actual screen, subtracting space between boxes
7575
double availableWidth = constraints.maxWidth;
76-
double spaceBetweenBoxes = 6.0; // Khoảng cách giữa các ô
77-
double totalSpace = spaceBetweenBoxes * 7; // Có 7 khoảng trống giữa 8 ô
78-
double boxWidth = (availableWidth - totalSpace) / 8; // Độ rộng tối đa mỗi ô
76+
double spaceBetweenBoxes = 6.0; // Space between boxes
77+
double totalSpace = spaceBetweenBoxes * 7; // 7 gaps between 8 boxes
78+
double boxWidth = (availableWidth - totalSpace) / 8; // Max width per box
7979

80-
// Khống chế độ rộng ô không quá to để nhìn cho art (max 35-40)
80+
// Constrain box width to look artistic (max 35-40)
8181
double finalBoxWidth = boxWidth > 38 ? 38 : boxWidth;
8282

8383
return Row(
84-
mainAxisAlignment: MainAxisAlignment.center, // Căn giữa hàng 8 ô
84+
mainAxisAlignment: MainAxisAlignment.center, // Center the row of 8 boxes
8585
children: List.generate(
86-
8, // SỬA: Đã tạo đúng 8 ô ở đây
86+
8, // FIX: Created 8 boxes here
8787
(index) => Padding(
8888
padding: EdgeInsets.symmetric(horizontal: spaceBetweenBoxes / 2),
8989
child: _buildOtpBox(index, context, finalBoxWidth),
@@ -94,26 +94,26 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
9494
),
9595
const SizedBox(height: 40),
9696

97-
// Nút xác nhận xịn sò (Nhận lỗi cụ thể từ Server)
97+
// Confirmation button (Handles specific Server errors)
9898
ElevatedButton(
9999
onPressed: _vm.isLoading
100100
? null
101101
: () async {
102102
FocusScope.of(context).unfocus();
103-
// Gọi hàm verify(), nó trả về String? errorMessage
103+
// Call verify(), it returns String? errorMessage
104104
final errorMessage = await _vm.verify();
105105
if (!context.mounted) return;
106106

107107
if (errorMessage == null) {
108-
// Thành công: Nhảy sang bước 3 (Đổi mật khẩu mới)
108+
// Success: Navigate to step 3 (Reset new password)
109109
Navigator.pushReplacement(
110110
context,
111111
MaterialPageRoute(
112112
builder: (_) => const NewPasswordView(),
113113
),
114114
);
115115
} else {
116-
// Thất bại: Hiện thông báo lỗi cụ thể (ví dụ: "Mã OTP hết hạn")
116+
// Failure: Show specific error message (e.g., "OTP code expired")
117117
ScaffoldMessenger.of(context).showSnackBar(
118118
SnackBar(
119119
content: Text(errorMessage),
@@ -134,7 +134,7 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
134134
color: AppColors.white,
135135
)
136136
: const Text(
137-
'XÁC NHẬN',
137+
'CONFIRM',
138138
style: TextStyle(
139139
fontSize: 16,
140140
fontWeight: FontWeight.bold,
@@ -144,7 +144,7 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
144144
),
145145
const SizedBox(height: 16),
146146

147-
// --- NÚT GỬI LẠI MÃ (CHỈ CÓ Ở BẢN XỊN) ---
147+
// --- RESEND CODE BUTTON ---
148148
TextButton.icon(
149149
onPressed: _vm.isLoading
150150
? null
@@ -155,7 +155,7 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
155155
if (errorMessage == null) {
156156
ScaffoldMessenger.of(context).showSnackBar(
157157
const SnackBar(
158-
content: Text('Đã gửi lại mã OTP!'),
158+
content: Text('OTP code resent!'),
159159
backgroundColor: AppColors.success,
160160
),
161161
);
@@ -170,7 +170,7 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
170170
},
171171
icon: const Icon(Icons.refresh, size: 18, color: AppColors.primary),
172172
label: const Text(
173-
'Gửi lại mã',
173+
'Resend code',
174174
style: TextStyle(color: AppColors.primary, fontWeight: FontWeight.bold),
175175
),
176176
)
@@ -184,10 +184,10 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
184184
);
185185
}
186186

187-
// SỬA: Nhận thêm 'boxWidth' để tự động co dãn cho vừa 8 ô
187+
// FIX: Added 'boxWidth' to automatically scale for 8 boxes
188188
Widget _buildOtpBox(int index, BuildContext context, double boxWidth) {
189189
return Container(
190-
width: boxWidth, // Sử dụng độ rộng đã tính toán
190+
width: boxWidth, // Use calculated width
191191
height: 48,
192192
decoration: BoxDecoration(
193193
color: AppColors.white,
@@ -197,7 +197,7 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
197197
child: TextField(
198198
onChanged: (value) {
199199
_vm.updateDigit(index, value);
200-
// SỬA: Logic nhảy focus chuẩn cho 8 ô (index chạy từ 0 đến 7)
200+
// FIX: Correct focus jump logic for 8 boxes (index from 0 to 7)
201201
if (value.isNotEmpty && index < 7) {
202202
FocusScope.of(context).nextFocus();
203203
}
@@ -223,4 +223,4 @@ class _OtpVerificationViewState extends State<OtpVerificationView> {
223223
),
224224
);
225225
}
226-
}
226+
}

src/lib/features/auth/presentation/viewmodels/auth_viewmodels.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import 'package:flutter/material.dart';
44
import 'package:supabase_flutter/supabase_flutter.dart';
5-
import '../../data/auth_helper.dart';
5+
import '../../services/auth_helper.dart';
66

77
// ==========================================
88
// BASE VIEWMODEL (Handles Loading, Validation & Errors)

src/lib/features/auth/data/auth_helper.dart renamed to src/lib/features/auth/services/auth_helper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// data/helpers/auth_helper.dart
1+
// services/helpers/auth_helper.dart
22

33
import 'package:supabase_flutter/supabase_flutter.dart';
44
import 'package:flutter_timezone/flutter_timezone.dart';

src/lib/features/auth/viewmodels/auth_viewmodels.dart

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ class LoginViewModel extends BaseViewModel {
1818

1919
void togglePass() { obscurePass = !obscurePass; notifyListeners(); }
2020

21-
Future<bool> login() async {
22-
if (emailCtrl.text.isEmpty || passCtrl.text.isEmpty) return false;
21+
Future<String?> login() async {
22+
if (emailCtrl.text.isEmpty || passCtrl.text.isEmpty) return "Please enter all fields";
2323
setLoading(true);
2424
// Mock API call
25-
await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
25+
await Future.delayed(const Duration(seconds: 1));
2626
setLoading(false);
27-
return true; // Assume success
27+
return null; // Success
2828
}
2929
}
3030

@@ -37,38 +37,53 @@ class RegisterViewModel extends BaseViewModel {
3737

3838
void togglePass() { obscurePass = !obscurePass; notifyListeners(); }
3939

40-
Future<bool> register() async {
41-
if (passCtrl.text != confirmPassCtrl.text) return false;
40+
Future<String?> register() async {
41+
if (passCtrl.text != confirmPassCtrl.text) return "Passwords do not match";
4242
setLoading(true);
43-
await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
43+
await Future.delayed(const Duration(seconds: 1));
4444
setLoading(false);
45-
return true;
45+
return null;
4646
}
4747
}
4848

4949
class ForgotPassViewModel extends BaseViewModel {
5050
final emailCtrl = TextEditingController();
5151

52-
Future<bool> sendCode() async {
53-
if (emailCtrl.text.isEmpty) return false;
52+
Future<String?> sendCode() async {
53+
if (emailCtrl.text.isEmpty) return "Please enter email";
5454
setLoading(true);
55-
await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
55+
await Future.delayed(const Duration(seconds: 1));
5656
setLoading(false);
57-
return true;
57+
return null;
5858
}
5959
}
6060

6161
class OtpViewModel extends BaseViewModel {
62-
List<String> digits = List.filled(6, "");
62+
// Updated to 8 digits to match UI
63+
List<String> digits = List.filled(8, "");
64+
65+
void updateDigit(int idx, String val) {
66+
if (idx < digits.length) {
67+
digits[idx] = val;
68+
notifyListeners();
69+
}
70+
}
6371

64-
void updateDigit(int idx, String val) { digits[idx] = val; notifyListeners(); }
72+
Future<String?> verify() async {
73+
String code = digits.join();
74+
if (code.length < 8) return "Please enter 8-digit OTP";
75+
setLoading(true);
76+
await Future.delayed(const Duration(seconds: 1));
77+
setLoading(false);
78+
return null; // Success
79+
}
6580

66-
Future<bool> verify() async {
67-
if (digits.join().length < 6) return false;
81+
// Added resend method to fix error in UI
82+
Future<String?> resend() async {
6883
setLoading(true);
69-
await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
84+
await Future.delayed(const Duration(seconds: 1));
7085
setLoading(false);
71-
return true;
86+
return null; // Success
7287
}
7388
}
7489

@@ -79,11 +94,11 @@ class NewPassViewModel extends BaseViewModel {
7994

8095
void togglePass() { obscurePass = !obscurePass; notifyListeners(); }
8196

82-
Future<bool> updatePassword() async {
83-
if (passCtrl.text != confirmPassCtrl.text) return false;
97+
Future<String?> updatePassword() async {
98+
if (passCtrl.text != confirmPassCtrl.text) return "Passwords do not match";
8499
setLoading(true);
85-
await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
100+
await Future.delayed(const Duration(seconds: 1));
86101
setLoading(false);
87-
return true;
102+
return null;
88103
}
89-
}
104+
}

src/lib/features/main/view/screens/main_screen.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
22
import 'package:task_management_app/features/statistics/viewmodel/statistics_viewmodel.dart';
33
import 'package:task_management_app/features/tasks/view/screens/home_screen.dart';
44
import '../../../../core/theme/app_colors.dart';
5+
import '../../../note/view/focus_screen.dart';
6+
import '../../../note/viewmodel/focus_viewmodel.dart';
57
import '../../../statistics/view/screens/statistics_screen.dart';
68
// import '../../../tasks/view/screens/home_screen.dart';
79
import 'package:provider/provider.dart';
@@ -19,7 +21,10 @@ class _MainScreenState extends State<MainScreen> {
1921
final List<Widget> _screens = [
2022
const Center(child: HomeScreen()),
2123
const Center(child: Text('Màn hình Lịch')),
22-
const Center(child: Text('Màn hình Tập trung')),
24+
ChangeNotifierProvider(
25+
create: (_) => FocusViewModel(),
26+
child: const FocusScreen(),
27+
),
2328
ChangeNotifierProvider(
2429
create: (_) => StatisticsViewmodel(),
2530
child: const StatisticsScreen(),
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class NoteModel {
2+
final String id;
3+
final String content;
4+
final bool pinned;
5+
final String? imagePath;
6+
7+
NoteModel({
8+
required this.id,
9+
required this.content,
10+
this.pinned = false,
11+
this.imagePath,
12+
});
13+
}

0 commit comments

Comments
 (0)