diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..74a2a63 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "languageToolLinter.languageTool.ignoredWordsInWorkspace": [ + "hijri" + ] +} \ No newline at end of file diff --git a/MIGRATIONS.md b/MIGRATIONS.md new file mode 100644 index 0000000..a471725 --- /dev/null +++ b/MIGRATIONS.md @@ -0,0 +1,10 @@ +# Migrations + +This document contains a list of migrations that were undertaken by Ramadan Taskminder +to update the data structure. The migration index prior to version 1.2.0 was `-1`. +The following list is in descending order. + +- `0`: Add the hijri date to the date field of Qur'an entries to support hijri date + offsets + + `[ "date", [ ... ] ]` => `[ [ "gregorian", "hijri" ], [ ... ] ]` diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 43ff8a2..ddf946a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -39,4 +39,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2aec6e9..cc954ff 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826d..5e31d3d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ boxes = ["quran", "tasks", "prayers"]; Uri feedbackUrl = Uri.parse( diff --git a/lib/date.dart b/lib/date.dart new file mode 100644 index 0000000..bf32e3c --- /dev/null +++ b/lib/date.dart @@ -0,0 +1,17 @@ +import 'package:jhijri/jHijri.dart'; + +JHijri jHijriFromIso8601Style(String iso8601) { + final components = iso8601.split("-"); + if (components.length != 3) { + print("Invalid ISO-8601 style string — \"$iso8601\""); + throw Error(); + } + + final numericalComponents = components.map((e) => int.parse(e)); + + return JHijri( + fYear: numericalComponents.elementAt(0), + fMonth: numericalComponents.elementAt(1), + fDay: numericalComponents.elementAt(2), + ); +} diff --git a/lib/extensions/date.dart b/lib/extensions/date.dart index e34cdae..d3a7671 100644 --- a/lib/extensions/date.dart +++ b/lib/extensions/date.dart @@ -1,3 +1,48 @@ +import 'package:jhijri/jHijri.dart'; + extension Date on DateTime { String getYMD() => toIso8601String().split("T")[0]; + + DateTime offset(int offset) { + if (offset == 1) { + return add(const Duration(days: 1)); + } else if (offset == -1) { + return subtract(const Duration(days: 1)); + } + + return this; + } +} + +extension JHijriDateTime on JHijri { + String hijriMonth() { + switch (month) { + case 1: + return "Muharram"; + case 2: + return "Safar"; + case 3: + return "Rabi' Al-Awwal"; + case 4: + return "Rabi' Al-Thani"; + case 5: + return "Jumada Al-Awwal"; + case 6: + return "Jumada Al-Thani"; + case 7: + return "Rajab"; + case 8: + return "Sha'aban"; + case 9: + return "Ramadan"; + case 10: + return "Shawwal"; + case 11: + return "Dhu Al-Qi'dah"; + case 12: + return "Dhu Al-Hijjah"; + default: + return ""; + } + } } diff --git a/lib/main.dart b/lib/main.dart index 748dafe..d3edbc2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ Future main() async { await Hive.openBox("tasks"); await Hive.openBox("quran"); await Hive.openBox("prayers"); + await Hive.openBox("settings"); runApp(const RamadanTaskminder()); } @@ -66,9 +67,9 @@ class RamadanTaskminder extends StatelessWidget { return MaterialApp.router( title: appName, theme: ThemeData( + useMaterial3: true, primaryColor: getPrimaryColor(context), scaffoldBackgroundColor: backgroundColor, - useMaterial3: true, textTheme: GoogleFonts.mPlus1pTextTheme(), ), darkTheme: ThemeData( @@ -76,9 +77,11 @@ class RamadanTaskminder extends StatelessWidget { brightness: Brightness.dark, scaffoldBackgroundColor: Colors.black, colorScheme: const ColorScheme.dark( - primary: Colors.black, - onPrimary: Colors.black, - secondary: primaryDarkColor, + primary: primaryDarkColor, + onPrimary: Colors.white, + surface: Colors.black, + onSurface: Colors.white, + secondary: primaryLightColor, ), cardTheme: const CardTheme( color: Colors.black87, @@ -98,6 +101,11 @@ class RamadanTaskminder extends StatelessWidget { ), ), ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: buttonTextDarkColor, + ), + ), textSelectionTheme: const TextSelectionThemeData( cursorColor: primaryDarkColor, selectionColor: primaryDarkColor, diff --git a/lib/screens/home.dart b/lib/screens/home.dart index c17c92c..8859d12 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:hijri/hijri_calendar.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; +import 'package:jhijri/jHijri.dart'; import 'package:ramadan_taskminder/constants.dart'; import 'package:ramadan_taskminder/extensions/date.dart'; +import 'package:ramadan_taskminder/extensions/int.dart'; import 'package:ramadan_taskminder/prayers.dart'; import 'package:ramadan_taskminder/quran.dart'; import 'package:ramadan_taskminder/theme.dart'; @@ -27,33 +28,82 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - DateTime current = DateTime.now(); - HijriCalendar hijriCurrent = HijriCalendar.now(); - Box tasksBox = Hive.box("tasks"); Box prayersBox = Hive.box("prayers"); Box quran = Hive.box("quran"); + Box settingsBox = Hive.box("settings"); late List allTasks; late Map tasks; late Map prayers; + DateTime current = DateTime.now(); + late JHijri hijriCurrent; + @override void initState() { super.initState(); - firstRun(); + + startup(); + initializeSettings(); initializeTasks(); initializeHistory(); initializePrayers(); } - void firstRun() async { + void startup() async { SharedPreferences preferences = await SharedPreferences.getInstance(); bool firstRun = preferences.getBool("firstRun") ?? true; if (firstRun) { tasksBox.put("allTasks", initialTasks); preferences.setBool("firstRun", false); } + + int migrationIndex = preferences.getInt("migrationIndex") ?? -1; + // print("Migration Index (setting): $migrationIndex, migration index (app): $currentMigrationIndex"); + if (migrationIndex < currentMigrationIndex) { + // print("Running migrations..."); + final migrations = migrationIndex.upTo(currentMigrationIndex).skip(1); + for (final migration in migrations) { + // print("Running migration #$migration..."); + migrate(migration); + } + } + } + + void migrate(int migrationIndex) { + if (migrationIndex == 0) { + List? quranHistory = quran.get("history"); + if (quranHistory != null) { + List newHistory = quranHistory.map((entry) { + if (entry[0].runtimeType == List) { + return entry; + } + + final gregorianDate = DateTime.parse(entry[0].toString()); + final hijriDate = + JHijri(fDate: gregorianDate, fDisplay: DisplayFormat.YYYYMMDD); + + return [ + [entry[0], hijriDate.fullDate], + entry[1] + ]; + }).toList(); + + // print("Old: $quranHistory"); + // print("New: $newHistory"); + quran.put("history", newHistory); + } + } + } + + void initializeSettings() { + hijriCurrent = JHijri( + fDate: DateTime.now().offset( + settingsBox.get("dateOffset", defaultValue: 0), + ), + fDisplay: DisplayFormat.YYYYMMDD, + ); } void initializeTasks() { @@ -172,7 +222,7 @@ class _HomeScreenState extends State { PageHeader( header: appName, title: - "${hijriCurrent.hDay} ${hijriCurrent.longMonthName} ${hijriCurrent.hYear}", + "${hijriCurrent.day} ${hijriCurrent.hijriMonth()} ${hijriCurrent.year}", rightAlign: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/quran.dart b/lib/screens/quran.dart index c0fa73e..82b14a8 100644 --- a/lib/screens/quran.dart +++ b/lib/screens/quran.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:hijri/hijri_calendar.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; +import 'package:jhijri/jHijri.dart'; +import 'package:ramadan_taskminder/date.dart'; +import 'package:ramadan_taskminder/extensions/date.dart'; import 'package:ramadan_taskminder/theme.dart'; import 'package:ramadan_taskminder/quran.dart'; import 'package:ramadan_taskminder/widgets/page_footer.dart'; @@ -53,8 +55,8 @@ class _QuranScreenState extends State { } else { history = quranHistory; history.sort( - (a, b) => DateTime.parse(a[0].toString()) - .compareTo(DateTime.parse(b[0].toString())), + (a, b) => DateTime.parse(a[0][0].toString()) + .compareTo(DateTime.parse(b[0][0].toString())), ); } } @@ -90,8 +92,8 @@ class _QuranScreenState extends State { context: context, builder: (BuildContext context) { final entry = history[deletingHistoryEntry]; - final date = DateTime.parse(entry[0].toString()); - final hijriDate = HijriCalendar.fromDate(date); + final date = DateTime.parse(entry[0][0].toString()); + final hijriDate = jHijriFromIso8601Style(entry[0][1]); final starting = (entry[1] as List)[0].split("-"); final ending = (entry[1] as List)[1].split("-"); @@ -106,7 +108,7 @@ class _QuranScreenState extends State { "Are you sure you want to delete the following entry?"), StackedCard( header: - "${DateFormat.MMMMd().format(date)} / ${hijriDate.longMonthName} ${hijriDate.hDay}", + "${DateFormat.MMMMd().format(date)} / ${hijriDate.hijriMonth()} ${hijriDate.day}", title: "${surahs[int.parse(starting[0]) - 1]["name"].toString()} ${starting[1]} - ${surahs[int.parse(ending[0]) - 1]["name"].toString()} ${ending[1]}", fullWidth: true, @@ -264,9 +266,19 @@ class _QuranScreenState extends State { onTap: () => setState( () { final entry = [ - DateTime.now() - .toIso8601String() - .split("T")[0], + [ + DateTime.now() + .toIso8601String() + .split("T")[0], + JHijri( + fDate: + DateTime.now().subtract( + const Duration(days: 1), + ), + fDisplay: + DisplayFormat.YYYYMMDD, + ).fullDate, + ], [ "${startingSurah + 1}-$startingAyah", "${endingSurah + 1}-$endingAyah" @@ -305,10 +317,11 @@ class _QuranScreenState extends State { runSpacing: 10, children: history.reversed.map( (entry) { - final date = - DateTime.parse(entry[0].toString()); + final date = DateTime.parse( + entry[0][0].toString(), + ); final hijriDate = - HijriCalendar.fromDate(date); + jHijriFromIso8601Style(entry[0][1]); final starting = (entry[1] as List)[0] @@ -319,7 +332,7 @@ class _QuranScreenState extends State { return StackedCard( header: - "${DateFormat.MMMMd().format(date)} / ${hijriDate.longMonthName} ${hijriDate.hDay}", + "${DateFormat.MMMMd().format(date)} / ${hijriDate.hijriMonth()} ${hijriDate.day}", title: "${surahs[int.parse(starting[0]) - 1]["name"].toString()} ${starting[1]} - ${surahs[int.parse(ending[0]) - 1]["name"].toString()} ${ending[1]}", fullWidth: true, diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 06694cd..b2678e5 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -24,7 +24,10 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { Box tasksBox = Hive.box("tasks"); + Box settingsBox = Hive.box("settings"); + late List allTasks; + late int dateOffset; String version = ""; String buildNumber = ""; @@ -33,6 +36,7 @@ class _SettingsScreenState extends State { void initState() { super.initState(); initializeTasks(); + calculateOffset(); loadPackageInfo(); } @@ -41,6 +45,101 @@ class _SettingsScreenState extends State { tasksBox.get("allTasks", defaultValue: initialTasks) as List; } + void calculateOffset() { + setState(() => dateOffset = settingsBox.get("dateOffset", defaultValue: 0)); + } + + void editDateOffset() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("Edit Hijri Date Offset"), + content: StatefulBuilder( + builder: ( + BuildContext context, + StateSetter setState, + ) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + RadioListTile( + title: const Text("-1 day"), + value: -1, + groupValue: dateOffset, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + contentPadding: const EdgeInsets.all(0.0), + onChanged: (value) { + setState(() { + if (value != null) { + settingsBox.put("dateOffset", value); + calculateOffset(); + } + }); + }, + ), + RadioListTile( + title: const Text("0 days"), + value: 0, + groupValue: dateOffset, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + contentPadding: const EdgeInsets.all(0.0), + onChanged: (value) { + setState(() { + if (value != null) { + settingsBox.put("dateOffset", value); + calculateOffset(); + } + }); + }, + ), + RadioListTile( + title: const Text("1 day"), + value: 1, + groupValue: dateOffset, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + contentPadding: const EdgeInsets.all(0.0), + onChanged: (value) { + setState(() { + if (value != null) { + settingsBox.put("dateOffset", value); + calculateOffset(); + } + }); + }, + ), + ], + ); + }, + ), + actions: [ + TextButton( + child: Text( + "Set", + style: TextStyle( + color: isDark(context) ? primaryLightColor : primaryDarkColor, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + void loadPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); setState( @@ -111,6 +210,25 @@ class _SettingsScreenState extends State { ), ], ), + const SizedBox( + height: 15, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader( + title: "Hijri Date Offset", + buttonText: "Edit", + onClick: () => editDateOffset(), + ), + const SizedBox(height: 5), + Statistic( + statistic: + "$dateOffset day${dateOffset == 0 ? "s" : ""}", + ), + ], + ), const SizedBox( height: 25, ), diff --git a/pubspec.lock b/pubspec.lock index 46aa4d7..5bc9d62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -168,14 +168,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - hijri: - dependency: "direct main" - description: - name: hijri - sha256: "203eb7341e32789af6429877e88411b1526ef44c5d157ce4d3b930dd32c37ef1" - url: "https://pub.dev" - source: hosted - version: "3.0.0" hive: dependency: "direct main" description: @@ -224,6 +216,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + jhijri: + dependency: "direct main" + description: + name: jhijri + sha256: c2a28e803b1d07513a29953c6685b933235511a1f57966fd5bf66bfdae131f70 + url: "https://pub.dev" + source: hosted + version: "1.0.0" js: dependency: transitive description: @@ -240,6 +240,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -268,26 +292,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" package_info_plus: dependency: "direct main" description: @@ -308,10 +332,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -597,6 +621,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5e76178..9969307 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,6 @@ dependencies: flutter: sdk: flutter - hijri: ^3.0.0 hive: ^2.2.3 hive_flutter: ^1.1.0 lucide_icons_flutter: ^1.0.3 @@ -42,6 +41,7 @@ dependencies: package_info_plus: ^5.0.1 google_fonts: ^6.1.0 go_router: ^13.1.0 + jhijri: ^1.0.0 path_provider: ^2.1.2 flutter_file_dialog: ^3.0.2 archive: ^3.4.10