diff --git a/android/app/build.gradle b/android/app/build.gradle
index ada7789..498e3ba 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -10,16 +10,17 @@ plugins {
android {
namespace = "edu.todo.fall20240"
- compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
+ compileSdk = 34
+ ndkVersion = "27.0.12077973"
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ coreLibraryDesugaringEnabled true
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_1_8
+ jvmTarget = JavaVersion.VERSION_17
}
defaultConfig {
@@ -30,6 +31,7 @@ android {
targetSdk = 34
versionCode = flutter.versionCode
versionName = flutter.versionName
+ multiDexEnabled true
}
buildTypes {
@@ -44,3 +46,7 @@ android {
flutter {
source = "../.."
}
+
+dependencies {
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
+}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5bd846d..10bf27c 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,28 +1,29 @@
+
+
+ android:icon="@mipmap/ic_launcher"
+ android:label="todo_fall_2024_0">
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
-
-
+
+
-
-
+
+
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index e1ca574..afa1e8e 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
index 7fb86d7..8d679a6 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -18,11 +18,11 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "7.3.0" apply false
+ id "com.android.application" version "8.7.1" apply false
// START: FlutterFire Configuration
- id "com.google.gms.google-services" version "4.3.15" apply false
+ id "com.google.gms.google-services" version "4.4.2" apply false
// END: FlutterFire Configuration
- id "org.jetbrains.kotlin.android" version "1.7.10" apply false
+ id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}
include ":app"
diff --git a/lib/data/model/todo.dart b/lib/data/model/todo.dart
index 7172c13..a6914d6 100644
--- a/lib/data/model/todo.dart
+++ b/lib/data/model/todo.dart
@@ -1,11 +1,11 @@
import 'package:cloud_firestore/cloud_firestore.dart';
-// TODO: Use https://pub.dev/packages/equatable
class Todo {
final String id;
final DateTime? completedAt;
final String text;
final DateTime? timestamp;
+ final DateTime? dueDate;
final String userId;
Todo({
@@ -13,6 +13,7 @@ class Todo {
required this.completedAt,
required this.text,
required this.timestamp,
+ required this.dueDate,
required this.userId,
});
@@ -22,6 +23,7 @@ class Todo {
completedAt: (map['completedAt'] as Timestamp?)?.toDate(),
text: map['text']?.toString() ?? '',
timestamp: (map['timestamp'] as Timestamp?)?.toDate(),
+ dueDate: (map['dueDate'] as Timestamp?)?.toDate(),
userId: map['userId']?.toString() ?? '',
);
}
@@ -31,6 +33,7 @@ class Todo {
'completedAt': completedAt == null ? null : Timestamp.fromDate(completedAt!),
'text': text,
'timestamp': timestamp == null ? null : Timestamp.fromDate(timestamp!),
+ 'dueDate': dueDate == null ? null : Timestamp.fromDate(dueDate!),
'userId': userId,
};
}
diff --git a/lib/main.dart b/lib/main.dart
index 4eeb88c..973f521 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -2,11 +2,13 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:todo_fall_2024_0/firebase_options.dart';
import 'package:todo_fall_2024_0/screen/router/router_screen.dart';
+import 'package:timezone/data/latest.dart' as tz;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
+ tz.initializeTimeZones();
runApp(const MyApp());
}
diff --git a/lib/screen/details/details_screen.dart b/lib/screen/details/details_screen.dart
index d832fcc..9cddab3 100644
--- a/lib/screen/details/details_screen.dart
+++ b/lib/screen/details/details_screen.dart
@@ -1,10 +1,15 @@
import 'dart:developer';
+import 'package:app_settings/app_settings.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+import 'package:timezone/timezone.dart' as tz;
import 'package:todo_fall_2024_0/data/model/todo.dart';
import 'package:todo_fall_2024_0/screen/home/home_screen.dart';
+final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
+
class DetailsScreen extends StatefulWidget {
const DetailsScreen({
required this.todo,
@@ -21,11 +26,13 @@ class _DetailsScreenState extends State {
TextEditingController? _controller;
String? text;
+ DateTime? dueDate;
@override
void initState() {
super.initState();
text = widget.todo.text;
+ dueDate = widget.todo.dueDate;
_controller = TextEditingController(text: text);
}
@@ -74,6 +81,72 @@ class _DetailsScreenState extends State {
});
},
),
+ SizedBox(height: 16),
+ ListTile(
+ title: Text('Due date'),
+ subtitle: dueDate == null ? null : Text(dueDate?.toString() ?? ''),
+ trailing: dueDate == null
+ ? IconButton(
+ onPressed: () async {
+ final isGranted = await _requestNotificationPermission();
+ if (!context.mounted) return;
+
+ if (!isGranted) {
+ _showPermissionDeniedSnackbar(context);
+ return;
+ }
+
+ await _initializeNotifications();
+ if (!context.mounted) return;
+
+ final pickedDate = await showDatePicker(
+ context: context,
+ initialDate: DateTime.now(),
+ firstDate: DateTime.now(),
+ lastDate: DateTime(2100),
+ );
+ if (!context.mounted) return;
+
+ if (pickedDate != null) {
+ final pickedTime = await showTimePicker(
+ context: context,
+ initialTime: TimeOfDay.fromDateTime(DateTime.now()),
+ );
+
+ if (pickedTime != null) {
+ final dueDate = DateTime(
+ pickedDate.year,
+ pickedDate.month,
+ pickedDate.day,
+ pickedTime.hour,
+ pickedTime.minute,
+ );
+ _setDueDate(widget.todo.id, dueDate);
+ setState(() {
+ this.dueDate = dueDate;
+ });
+
+ await _scheduleNotification(
+ widget.todo.id,
+ dueDate,
+ widget.todo.text,
+ );
+ }
+ }
+ },
+ icon: Icon(Icons.add),
+ )
+ : IconButton(
+ onPressed: () {
+ _setDueDate(widget.todo.id, null);
+ setState(() {
+ dueDate = null;
+ });
+ _cancelNotification(widget.todo.id);
+ },
+ icon: Icon(Icons.close),
+ ),
+ ),
],
),
),
@@ -96,6 +169,83 @@ Future _updateTodo(String todoId, String text) async {
'text': text,
});
} catch (e, st) {
- log('Error deleting todo', error: e, stackTrace: st);
+ log('Error updating todo', error: e, stackTrace: st);
}
}
+
+Future _setDueDate(String todoId, DateTime? dueDate) async {
+ try {
+ await FirebaseFirestore.instance.collection(collectionTodo).doc(todoId).update({
+ 'dueDate': dueDate,
+ });
+ } catch (e, st) {
+ log('Error setting due date', error: e, stackTrace: st);
+ }
+}
+
+Future _requestNotificationPermission() async {
+ final isGranted = await flutterLocalNotificationsPlugin
+ .resolvePlatformSpecificImplementation()
+ ?.requestNotificationsPermission() ??
+ false;
+ return isGranted;
+}
+
+void _showPermissionDeniedSnackbar(BuildContext context) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ 'You need to enable notifications to set due date.',
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.white),
+ ),
+ backgroundColor: Colors.redAccent,
+ duration: Duration(seconds: 10),
+ action: SnackBarAction(
+ label: 'Open Settings',
+ textColor: Colors.white,
+ onPressed: () {
+ AppSettings.openAppSettings(
+ type: AppSettingsType.notification,
+ );
+ },
+ ),
+ ),
+ );
+}
+
+Future _initializeNotifications() async {
+ final initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
+ final InitializationSettings initializationSettings = InitializationSettings(
+ android: initializationSettingsAndroid,
+ );
+ await flutterLocalNotificationsPlugin.initialize(
+ initializationSettings,
+ );
+}
+
+Future _scheduleNotification(
+ String todoId,
+ DateTime dueDate,
+ String text,
+) async {
+ final tzDateTime = tz.TZDateTime.from(dueDate, tz.local);
+ await flutterLocalNotificationsPlugin.zonedSchedule(
+ todoId.hashCode,
+ 'Task due',
+ text,
+ tzDateTime,
+ const NotificationDetails(
+ android: AndroidNotificationDetails(
+ 'general_channel',
+ 'General Notifications',
+ ),
+ ),
+ androidScheduleMode: AndroidScheduleMode.inexact,
+ uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
+ matchDateTimeComponents: DateTimeComponents.dateAndTime,
+ );
+}
+
+Future _cancelNotification(String todoId) async {
+ await flutterLocalNotificationsPlugin.cancel(todoId.hashCode);
+}
diff --git a/lib/screen/home/widgets/todos_list.dart b/lib/screen/home/widgets/todos_list.dart
index cca4f5c..48d0879 100644
--- a/lib/screen/home/widgets/todos_list.dart
+++ b/lib/screen/home/widgets/todos_list.dart
@@ -34,6 +34,7 @@ class TodosList extends StatelessWidget {
decoration: isCompleted ? TextDecoration.lineThrough : null,
),
),
+ subtitle: todo.dueDate == null ? null : Text(todo.dueDate.toString()),
trailing: IconButton(
icon: Icon(Icons.chevron_right),
onPressed: () {
diff --git a/pubspec.lock b/pubspec.lock
index 4d49004..1615713 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -9,6 +9,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.44"
+ app_settings:
+ dependency: "direct main"
+ description:
+ name: app_settings
+ sha256: "09bc7fe0313a507087bec1a3baf555f0576e816a760cbb31813a88890a09d9e5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.1"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.6.0"
async:
dependency: transitive
description:
@@ -73,6 +89,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
+ dbus:
+ dependency: transitive
+ description:
+ name: dbus
+ sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.10"
fake_async:
dependency: transitive
description:
@@ -81,6 +105,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.3"
firebase_auth:
dependency: "direct main"
description:
@@ -142,6 +174,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
+ flutter_local_notifications:
+ dependency: "direct main"
+ description:
+ name: flutter_local_notifications
+ sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
+ url: "https://pub.dev"
+ source: hosted
+ version: "18.0.1"
+ flutter_local_notifications_linux:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_linux
+ sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
+ flutter_local_notifications_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_platform_interface
+ sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -288,6 +344,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.2"
plugin_platform_interface:
dependency: transitive
description:
@@ -357,6 +421,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.2"
+ timezone:
+ dependency: "direct main"
+ description:
+ name: timezone
+ sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.10.0"
typed_data:
dependency: transitive
description:
@@ -389,6 +461,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
+ xdg_directories:
+ dependency: transitive
+ description:
+ name: xdg_directories
+ sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.5.0"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.22.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 58903f5..00dac0c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,6 +15,9 @@ dependencies:
sign_in_button: ^3.2.0
google_sign_in: ^6.2.1
cloud_firestore: ^5.4.4
+ flutter_local_notifications: ^18.0.1
+ app_settings: ^5.1.1
+ timezone: ^0.10.0
dev_dependencies: