Skip to content

Commit 8a3e2e1

Browse files
committed
Remakeing application - 2026-01-10 [2]
1 parent 589b054 commit 8a3e2e1

18 files changed

Lines changed: 367 additions & 106 deletions

android/.kotlin/sessions/kotlin-compiler-8468629118357355569.salive

Whitespace-only changes.

android/app/build.gradle.kts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
plugins {
22
id("com.android.application")
33
id("kotlin-android")
4-
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
54
id("dev.flutter.flutter-gradle-plugin")
65
}
76

@@ -13,32 +12,32 @@ android {
1312
compileOptions {
1413
sourceCompatibility = JavaVersion.VERSION_17
1514
targetCompatibility = JavaVersion.VERSION_17
15+
isCoreLibraryDesugaringEnabled = true
1616
}
1717

1818
kotlinOptions {
1919
jvmTarget = JavaVersion.VERSION_17.toString()
2020
}
2121

2222
defaultConfig {
23-
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
2423
applicationId = "com.example.note_alarm"
25-
// You can update the following values to match your application needs.
26-
// For more information, see: https://flutter.dev/to/review-gradle-config.
27-
minSdk = flutter.minSdkVersion
24+
minSdk = flutter.minSdkVersion // REQUIRED for notifications + desugaring
2825
targetSdk = flutter.targetSdkVersion
2926
versionCode = flutter.versionCode
3027
versionName = flutter.versionName
3128
}
3229

3330
buildTypes {
3431
release {
35-
// TODO: Add your own signing config for the release build.
36-
// Signing with the debug keys for now, so `flutter run --release` works.
3732
signingConfig = signingConfigs.getByName("debug")
3833
}
3934
}
4035
}
4136

37+
dependencies {
38+
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
39+
}
40+
4241
flutter {
4342
source = "../.."
4443
}

android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,6 @@
5151
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
5252
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
5353
<uses-permission android:name="android.permission.WAKE_LOCK"/>
54+
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
55+
5456
</manifest>

android/app/src/profile/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
to allow setting breakpoints, to provide hot reload, etc.
55
-->
66
<uses-permission android:name="android.permission.INTERNET"/>
7+
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
78
</manifest>

lib/app.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class NoteAlarmApp extends StatelessWidget {
99
@override
1010
Widget build(BuildContext context) {
1111
return MaterialApp(
12-
title: 'Note-Alarm',
12+
title: 'Note Alarm',
1313
debugShowCheckedModeBanner: false,
1414
theme: AppTheme.lightTheme,
1515
home: const HomeShell(),

lib/core/notifications/notification_service.dart

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:io';
12
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
23
import 'package:timezone/data/latest.dart' as tz;
34
import 'package:timezone/timezone.dart' as tz;
@@ -6,18 +7,57 @@ class NotificationService {
67
static final FlutterLocalNotificationsPlugin _plugin =
78
FlutterLocalNotificationsPlugin();
89

10+
/// Initialize notifications + timezone
911
static Future<void> init() async {
1012
tz.initializeTimeZones();
1113

1214
const androidSettings = AndroidInitializationSettings(
1315
'@mipmap/launcher_icon',
1416
);
1517

16-
const initSettings = InitializationSettings(android: androidSettings);
18+
const settings = InitializationSettings(android: androidSettings);
1719

18-
await _plugin.initialize(initSettings);
20+
await _plugin.initialize(settings);
21+
22+
// 🔔 REQUIRED notification channel
23+
const channel = AndroidNotificationChannel(
24+
'alarm_channel',
25+
'Alarm Notifications',
26+
description: 'Exact alarm alerts',
27+
importance: Importance.max,
28+
sound: RawResourceAndroidNotificationSound('alarm_clock_1'),
29+
playSound: true,
30+
);
31+
32+
final android = _plugin
33+
.resolvePlatformSpecificImplementation<
34+
AndroidFlutterLocalNotificationsPlugin
35+
>();
36+
37+
await android?.createNotificationChannel(channel);
38+
}
39+
40+
/// Android 12+ requires user approval for exact alarms
41+
static Future<bool> requestExactAlarmPermission() async {
42+
if (!Platform.isAndroid) return true;
43+
44+
final androidPlugin = _plugin
45+
.resolvePlatformSpecificImplementation<
46+
AndroidFlutterLocalNotificationsPlugin
47+
>();
48+
49+
if (androidPlugin == null) return false;
50+
51+
final canSchedule = await androidPlugin.canScheduleExactNotifications();
52+
53+
if (canSchedule == true) return true;
54+
55+
// Opens system permission screen
56+
await androidPlugin.requestExactAlarmsPermission();
57+
return false;
1958
}
2059

60+
/// Schedule exact alarm (works when app is closed)
2161
static Future<void> scheduleAlarm({
2262
required int id,
2363
required DateTime dateTime,
@@ -32,8 +72,8 @@ class NotificationService {
3272
const NotificationDetails(
3373
android: AndroidNotificationDetails(
3474
'alarm_channel',
35-
'Alarms',
36-
channelDescription: 'Alarm notifications',
75+
'Alarm Notifications',
76+
channelDescription: 'Exact alarm alerts',
3777
importance: Importance.max,
3878
priority: Priority.high,
3979
playSound: true,
@@ -42,7 +82,6 @@ class NotificationService {
4282
),
4383
),
4484
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
45-
matchDateTimeComponents: DateTimeComponents.time, // ✅ NEW API
4685
);
4786
}
4887

lib/core/theme/app_theme.dart

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import 'package:flutter/material.dart';
22

3-
/// Centralized theme for easy scaling & branding
43
class AppTheme {
5-
static ThemeData get lightTheme {
6-
return ThemeData(
7-
useMaterial3: true,
8-
colorSchemeSeed: Colors.indigo,
9-
scaffoldBackgroundColor: Colors.grey.shade50,
10-
appBarTheme: const AppBarTheme(centerTitle: true),
11-
);
12-
}
4+
static ThemeData get lightTheme => ThemeData(
5+
useMaterial3: true,
6+
colorSchemeSeed: Colors.indigo,
7+
scaffoldBackgroundColor: Colors.grey.shade50,
8+
appBarTheme: const AppBarTheme(centerTitle: true),
9+
);
1310
}
Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
import 'package:flutter/material.dart';
22

3-
/// Alarm entity model (notification-backed)
43
class AlarmModel {
54
final TimeOfDay time;
6-
final int notificationId;
5+
final int id;
6+
final DateTime scheduledAt;
7+
bool active;
78

8-
AlarmModel({required this.time, required this.notificationId});
9+
AlarmModel({
10+
required this.time,
11+
required this.id,
12+
required this.scheduledAt,
13+
this.active = true,
14+
});
15+
16+
Map<String, dynamic> toJson() => {
17+
'hour': time.hour,
18+
'minute': time.minute,
19+
'id': id,
20+
'scheduledAt': scheduledAt.toIso8601String(),
21+
'active': active,
22+
};
23+
24+
factory AlarmModel.fromJson(Map<String, dynamic> json) {
25+
return AlarmModel(
26+
time: TimeOfDay(hour: json['hour'], minute: json['minute']),
27+
id: json['id'],
28+
scheduledAt: DateTime.parse(json['scheduledAt']),
29+
active: json['active'],
30+
);
31+
}
932
}

lib/features/Alarm/alarm_page.dart

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import 'dart:async';
12
import 'package:flutter/material.dart';
2-
import 'alarm_model.dart';
3+
34
import '../../core/notifications/notification_service.dart';
5+
import '../../shared/storage/alarm_storage.dart';
6+
import 'alarm_model.dart';
47

58
class AlarmScreen extends StatefulWidget {
69
const AlarmScreen({super.key});
@@ -10,9 +13,52 @@ class AlarmScreen extends StatefulWidget {
1013
}
1114

1215
class _AlarmScreenState extends State<AlarmScreen> {
13-
final List<AlarmModel> _alarms = [];
16+
final List<AlarmModel> alarms = [];
17+
Timer? _ticker;
18+
19+
@override
20+
void initState() {
21+
super.initState();
22+
_loadAlarms();
23+
_ticker = Timer.periodic(const Duration(minutes: 1), (_) {
24+
if (mounted) setState(() {});
25+
});
26+
}
27+
28+
@override
29+
void dispose() {
30+
_ticker?.cancel();
31+
super.dispose();
32+
}
33+
34+
Future<void> _loadAlarms() async {
35+
final stored = await AlarmStorage.load();
36+
if (!mounted) return;
37+
alarms.addAll(stored);
38+
setState(() {});
39+
}
40+
41+
Future<void> _persist() async {
42+
await AlarmStorage.save(alarms);
43+
}
44+
45+
Duration _remainingTime(AlarmModel alarm) {
46+
return alarm.scheduledAt.difference(DateTime.now());
47+
}
1448

1549
Future<void> _addAlarm() async {
50+
final allowed = await NotificationService.requestExactAlarmPermission();
51+
if (!allowed) {
52+
if (!mounted) return;
53+
ScaffoldMessenger.of(context).showSnackBar(
54+
const SnackBar(
55+
content: Text('Enable exact alarm permission in system settings'),
56+
),
57+
);
58+
return;
59+
}
60+
61+
if (!mounted) return;
1662
final picked = await showTimePicker(
1763
context: context,
1864
initialTime: TimeOfDay.now(),
@@ -29,12 +75,11 @@ class _AlarmScreenState extends State<AlarmScreen> {
2975
picked.minute,
3076
);
3177

32-
// If selected time already passed, schedule for tomorrow
3378
if (scheduled.isBefore(now)) {
3479
scheduled = scheduled.add(const Duration(days: 1));
3580
}
3681

37-
final id = scheduled.millisecondsSinceEpoch ~/ 1000;
82+
final int id = scheduled.millisecondsSinceEpoch ~/ 1000;
3883

3984
await NotificationService.scheduleAlarm(
4085
id: id,
@@ -43,20 +88,38 @@ class _AlarmScreenState extends State<AlarmScreen> {
4388
body: 'It\'s time!',
4489
);
4590

46-
setState(() {
47-
_alarms.add(AlarmModel(time: picked, notificationId: id));
91+
alarms.add(
92+
AlarmModel(time: picked, id: id, scheduledAt: scheduled, active: true),
93+
);
4894

49-
_alarms.sort((a, b) {
50-
final aMin = a.time.hour * 60 + a.time.minute;
51-
final bMin = b.time.hour * 60 + b.time.minute;
52-
return aMin.compareTo(bMin);
53-
});
54-
});
95+
setState(() {});
96+
await _persist();
97+
}
98+
99+
Future<void> _toggleAlarm(int index, bool value) async {
100+
final alarm = alarms[index];
101+
alarm.active = value;
102+
103+
if (value) {
104+
await NotificationService.scheduleAlarm(
105+
id: alarm.id,
106+
dateTime: alarm.scheduledAt,
107+
title: '⏰ Alarm',
108+
body: 'It\'s time!',
109+
);
110+
} else {
111+
await NotificationService.cancel(alarm.id);
112+
}
113+
114+
setState(() {});
115+
await _persist();
55116
}
56117

57118
Future<void> _deleteAlarm(int index) async {
58-
await NotificationService.cancel(_alarms[index].notificationId);
59-
setState(() => _alarms.removeAt(index));
119+
await NotificationService.cancel(alarms[index].id);
120+
alarms.removeAt(index);
121+
setState(() {});
122+
await _persist();
60123
}
61124

62125
@override
@@ -67,16 +130,17 @@ class _AlarmScreenState extends State<AlarmScreen> {
67130
icon: const Icon(Icons.add_alarm),
68131
label: const Text('Add Alarm'),
69132
),
70-
body: _alarms.isEmpty
133+
body: alarms.isEmpty
71134
? const Center(child: Text('No alarms set'))
72135
: ListView.builder(
73136
padding: const EdgeInsets.all(16),
74-
itemCount: _alarms.length,
137+
itemCount: alarms.length,
75138
itemBuilder: (context, index) {
76-
final alarm = _alarms[index];
139+
final alarm = alarms[index];
140+
final remaining = _remainingTime(alarm);
77141

78142
return Dismissible(
79-
key: ValueKey(alarm.notificationId),
143+
key: ValueKey(alarm.id),
80144
direction: DismissDirection.endToStart,
81145
onDismissed: (_) => _deleteAlarm(index),
82146
background: Container(
@@ -87,7 +151,7 @@ class _AlarmScreenState extends State<AlarmScreen> {
87151
),
88152
child: Card(
89153
shape: RoundedRectangleBorder(
90-
borderRadius: BorderRadius.circular(15),
154+
borderRadius: BorderRadius.circular(16),
91155
),
92156
child: ListTile(
93157
title: Text(
@@ -97,7 +161,16 @@ class _AlarmScreenState extends State<AlarmScreen> {
97161
fontWeight: FontWeight.bold,
98162
),
99163
),
100-
trailing: const Icon(Icons.notifications_active),
164+
subtitle: alarm.active && remaining.inMinutes > 0
165+
? Text(
166+
'${remaining.inHours}h ${remaining.inMinutes % 60}m remaining',
167+
style: const TextStyle(color: Colors.grey),
168+
)
169+
: const Text('Inactive'),
170+
trailing: Switch(
171+
value: alarm.active,
172+
onChanged: (v) => _toggleAlarm(index, v),
173+
),
101174
),
102175
),
103176
);

0 commit comments

Comments
 (0)