diff --git a/android/app/build.gradle b/android/app/build.gradle index 4bcbd91..d95a0f3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -29,7 +29,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 30 + compileSdkVersion 32 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -45,7 +45,7 @@ android { defaultConfig { applicationId "com.pavlenko.Habo" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 30 multiDexEnabled true versionCode flutterVersionCode.toInteger() @@ -78,7 +78,7 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.1' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7b4f418..fae8e40 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ FlutterApplication and put your custom class here. --> + diff --git a/assets/images/onboard/1.svg b/assets/images/onboard/1.svg new file mode 100644 index 0000000..0e81d59 --- /dev/null +++ b/assets/images/onboard/1.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/onboard/2.svg b/assets/images/onboard/2.svg new file mode 100644 index 0000000..569f356 --- /dev/null +++ b/assets/images/onboard/2.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/onboard/3.svg b/assets/images/onboard/3.svg new file mode 100644 index 0000000..02a20cc --- /dev/null +++ b/assets/images/onboard/3.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/sounds/sound_sources.txt b/assets/sounds/sound_sources.txt new file mode 100644 index 0000000..6f7b6d5 --- /dev/null +++ b/assets/sounds/sound_sources.txt @@ -0,0 +1,2 @@ +check.wav - https://freesound.org/s/456161/ +click.wav - https://freesound.org/s/268108/ \ No newline at end of file diff --git a/lib/backup.dart b/lib/backup.dart new file mode 100644 index 0000000..8ecb225 --- /dev/null +++ b/lib/backup.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:Habo/widgets/habit.dart'; +import 'package:path_provider/path_provider.dart'; + +class Backup { + static Future get _localFile async { + final directory = await getApplicationDocumentsDirectory(); + final path = directory.path; + return File('$path/backup.json'); + } + + static Future writeBackup(List input) async { + final file = await _localFile; + return file.writeAsString(jsonEncode(input)); + } + + static Future readBackup(String path) async { + try { + final file = File(path); + final contents = await file.readAsString(); + return contents; + } catch (e) { + return ""; + } + } +} diff --git a/lib/helpers.dart b/lib/helpers.dart index 477625f..099ee35 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -1,4 +1,5 @@ import 'package:Habo/screens/create_habit_screen.dart'; +import 'package:Habo/screens/onboarding_screen.dart'; import 'package:Habo/screens/settings_screen.dart'; import 'package:Habo/screens/statistics_screen.dart'; import 'package:flutter/material.dart'; @@ -18,6 +19,11 @@ Future navigateToCreatePage(context) async { context, MaterialPageRoute(builder: (context) => CreateHabitScreen())); } +Future navigateToOnboarding(context) async { + Navigator.push( + context, MaterialPageRoute(builder: (context) => OnBoardingScreen())); +} + TimeOfDay parseTimeOfDay(String value) { if (value != null) { var times = value.split(":"); @@ -30,6 +36,22 @@ TimeOfDay parseTimeOfDay(String value) { enum DayType { Clear, Check, Fail, Skip } +List months = [ + "", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + class HaboColors { static final Color primary = Color(0xFF09BF30); static final Color red = Colors.red; diff --git a/lib/main.dart b/lib/main.dart index 2370a1c..9376b27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,6 @@ import 'package:Habo/provider.dart'; import 'package:Habo/screens/home_screen.dart'; import 'package:Habo/screens/loading_screen.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; diff --git a/lib/model.dart b/lib/model.dart index 158595d..f3595e0 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -29,6 +29,29 @@ class HaboModel { } } + Future emptyTables() async { + try { + await db.delete("habits"); + await db.delete("events"); + } catch (_) { + print(_); + } + } + + Future useBackup(List habits) async { + try { + await emptyTables(); + habits.forEach((element) { + insertHabit(element); + element.habitData.events.forEach((key, value) { + insertEvent(element.habitData.id, key, value); + }); + }); + } catch (_) { + print(_); + } + } + Future editHabit(Habit habit) async { try { var id = await db.update( @@ -84,12 +107,10 @@ class HaboModel { return result; } - /// Update Company table V1 to V2 void _updateTableEventsV1toV2(Batch batch) { batch.execute('ALTER TABLE Events ADD comment TEXT'); } - /// Create Events table V2 void _createTableEventsV2(Batch batch) { batch.execute('DROP TABLE IF EXISTS events'); batch.execute('''CREATE TABLE events ( @@ -102,7 +123,6 @@ class HaboModel { )'''); } - /// Create Company table V2 void _createTableHabitsV2(Batch batch) { batch.execute('DROP TABLE IF EXISTS habits'); batch.execute('''CREATE TABLE habits ( diff --git a/lib/provider.dart b/lib/provider.dart index 8922651..795b89f 100644 --- a/lib/provider.dart +++ b/lib/provider.dart @@ -1,14 +1,16 @@ import 'dart:collection'; import 'dart:convert'; +import 'package:Habo/backup.dart'; import 'package:Habo/habit_data.dart'; +import 'package:Habo/helpers.dart'; import 'package:Habo/model.dart'; import 'package:Habo/notification_center.dart'; import 'package:Habo/settings_data.dart'; import 'package:Habo/statistics.dart'; import 'package:Habo/widgets/habit.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:just_audio/just_audio.dart'; import 'package:package_info/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -59,6 +61,60 @@ class Bloc with ChangeNotifier { } } + createBackup() async { + try { + var file = await Backup.writeBackup(allHabits); + final params = SaveFileDialogParams( + sourceFilePath: file.path, + mimeTypesFilter: ['application/json'], + ); + await FlutterFileDialog.saveFile(params: params); + } catch (e) { + showErrorMessage('ERROR: Creating backup failed.'); + } + } + + loadBackup() async { + try { + final params = OpenFileDialogParams( + fileExtensionsFilter: ['json'], + mimeTypesFilter: ['application/json'], + ); + final filePath = await FlutterFileDialog.pickFile(params: params); + if (filePath == null) { + return; + } + final json = await Backup.readBackup(filePath); + List habits = []; + jsonDecode(json).forEach((element) { + habits.add(Habit.fromJson(element)); + }); + await _haboModel.useBackup(habits); + removeNotifications(allHabits); + allHabits = habits; + resetNotifications(allHabits); + notifyListeners(); + } catch (e) { + showErrorMessage('ERROR: Restoring backup failed.'); + } + } + + resetNotifications(List habits) { + habits.forEach((element) { + if (element.habitData.notification) { + var data = element.habitData; + _notificationCenter.setHabitNotification( + data.id, data.notTime, 'Habo', data.title); + } + }); + } + + removeNotifications(List habits) { + habits.forEach((element) { + _notificationCenter.disableNotification(element.habitData.id); + }); + } + List get getAllHabits { return allHabits; } @@ -91,6 +147,14 @@ class Bloc with ChangeNotifier { return settingsData.getSoundEffects; } + bool get getShowMonthName { + return settingsData.getShowMonthName; + } + + bool get getSeenOnboarding { + return settingsData.getSeenOnboarding; + } + String get getTheme { return settingsData.getTheme; } @@ -111,24 +175,30 @@ class Bloc with ChangeNotifier { return settingsData.getWeekStartList; } - set setDailyNot(TimeOfDay value) { - settingsData.setDailyNot = value; + set setSeenOnboarding(bool value) { + settingsData.setSeenOnboarding = value; + _saveSharedPreferences(); + notifyListeners(); + } + + _saveSharedPreferences() async { _prefs.then((SharedPreferences prefs) { var st = settingsData.toJson().toString(); prefs.remove('habo_settings'); prefs.setString('habo_settings', st); }); + } + + set setDailyNot(TimeOfDay value) { + settingsData.setDailyNot = value; + _saveSharedPreferences(); _notificationCenter.setNotification(settingsData.getDailyNot); notifyListeners(); } set setShowDailyNot(bool value) { settingsData.setShowDailyNot = value; - _prefs.then((SharedPreferences prefs) { - var st = settingsData.toJson().toString(); - prefs.remove('habo_settings'); - prefs.setString('habo_settings', st); - }); + _saveSharedPreferences(); if (value) { _notificationCenter.setNotification(settingsData.getDailyNot); } else { @@ -139,31 +209,25 @@ class Bloc with ChangeNotifier { set setSoundEffects(bool value) { settingsData.setSoundEffects = value; - _prefs.then((SharedPreferences prefs) { - var st = settingsData.toJson().toString(); - prefs.remove('habo_settings'); - prefs.setString('habo_settings', st); - }); + _saveSharedPreferences(); + notifyListeners(); + } + + set setShowMonthName(bool value) { + settingsData.setShowMonthName = value; + _saveSharedPreferences(); notifyListeners(); } set setTheme(String value) { settingsData.setTheme = value; - _prefs.then((SharedPreferences prefs) { - var st = settingsData.toJson().toString(); - prefs.remove('habo_settings'); - prefs.setString('habo_settings', st); - }); + _saveSharedPreferences(); notifyListeners(); } set setWeekStart(String value) { settingsData.setWeekStart = value; - _prefs.then((SharedPreferences prefs) { - var st = settingsData.toJson().toString(); - prefs.remove('habo_settings'); - prefs.setString('habo_settings', st); - }); + _saveSharedPreferences(); notifyListeners(); } @@ -342,4 +406,16 @@ class Bloc with ChangeNotifier { } }); } + + showErrorMessage(String message) { + _scaffoldKey.currentState.hideCurrentSnackBar(); + _scaffoldKey.currentState.showSnackBar( + SnackBar( + duration: Duration(seconds: 3), + content: Text(message), + behavior: SnackBarBehavior.floating, + backgroundColor: HaboColors.red, + ), + ); + } } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index c40e7db..794c48b 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,5 +1,6 @@ import 'package:Habo/helpers.dart'; import 'package:Habo/provider.dart'; +import 'package:Habo/screens/onboarding_screen.dart'; import 'package:Habo/widgets/calendar_column.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -7,55 +8,55 @@ import 'package:provider/provider.dart'; class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - child: Scaffold( - appBar: AppBar( - title: Text( - "Habo", - style: Theme.of(context).textTheme.headline5, - ), - backgroundColor: Colors.transparent, - actions: [ - IconButton( - icon: const Icon( - Icons.bar_chart, - semanticLabel: 'Statistics', + return (!Provider.of(context).getSeenOnboarding) + ? OnBoardingScreen() + : Scaffold( + appBar: AppBar( + title: Text( + "Habo", + style: Theme.of(context).textTheme.headline5, ), - color: Colors.grey[400], - tooltip: 'Statistics', - onPressed: () { - Provider.of(context, listen: false).hideSnackBar(); - navigateToStatisticsPage(context); - }, + backgroundColor: Colors.transparent, + actions: [ + IconButton( + icon: const Icon( + Icons.bar_chart, + semanticLabel: 'Statistics', + ), + color: Colors.grey[400], + tooltip: 'Statistics', + onPressed: () { + Provider.of(context, listen: false).hideSnackBar(); + navigateToStatisticsPage(context); + }, + ), + IconButton( + icon: const Icon( + Icons.settings, + semanticLabel: 'Settings', + ), + color: Colors.grey[400], + tooltip: 'Settings', + onPressed: () { + Provider.of(context, listen: false).hideSnackBar(); + navigateToSettingsPage(context); + }, + ), + ], ), - IconButton( - icon: const Icon( - Icons.settings, - semanticLabel: 'Settings', - ), - color: Colors.grey[400], - tooltip: 'Settings', + body: CalendarColumn(), + floatingActionButton: FloatingActionButton( onPressed: () { Provider.of(context, listen: false).hideSnackBar(); - navigateToSettingsPage(context); + navigateToCreatePage(context); }, + child: Icon( + Icons.add, + color: Colors.white, + semanticLabel: 'Add', + size: 35.0, + ), ), - ], - ), - body: CalendarColumn(), - floatingActionButton: FloatingActionButton( - onPressed: () { - Provider.of(context, listen: false).hideSnackBar(); - navigateToCreatePage(context); - }, - child: Icon( - Icons.add, - color: Colors.white, - semanticLabel: 'Add', - size: 35.0, - ), - ), - ), - ); + ); } } diff --git a/lib/screens/loading_screen.dart b/lib/screens/loading_screen.dart index c21320a..230b356 100644 --- a/lib/screens/loading_screen.dart +++ b/lib/screens/loading_screen.dart @@ -3,23 +3,21 @@ import 'package:flutter/material.dart'; class LoadingScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - child: Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Image.asset( - 'assets/images/icon.png', - width: 72, - ), - Text( - "Habo", - style: Theme.of(context).textTheme.headline5, - ), - ], - ), + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'assets/images/icon.png', + width: 72, + ), + Text( + "Habo", + style: Theme.of(context).textTheme.headline5, + ), + ], ), ), ); diff --git a/lib/screens/onboarding_screen.dart b/lib/screens/onboarding_screen.dart new file mode 100644 index 0000000..536019e --- /dev/null +++ b/lib/screens/onboarding_screen.dart @@ -0,0 +1,186 @@ +import 'package:Habo/helpers.dart'; +import 'package:Habo/provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:introduction_screen/introduction_screen.dart'; +import 'package:provider/provider.dart'; + +class OnBoardingScreen extends StatelessWidget { + final List listPagesViewModel = [ + PageViewModel( + titleWidget: Text( + 'Define your habits', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + image: Container( + child: SvgPicture.asset( + 'assets/images/onboard/1.svg', + semanticsLabel: 'Empty list', + width: 250, + ), + ), + bodyWidget: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Text( + 'To better stick to your habits, you can define:', + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '1. Cue', + style: TextStyle(fontSize: 18), + ), + SizedBox(height: 5), + Text( + '2. Routine', + style: TextStyle(fontSize: 18), + ), + SizedBox(height: 5), + Text( + '3. Reward', + style: TextStyle(fontSize: 18), + ) + ], + ), + ], + ), + ), + ), + ), + PageViewModel( + titleWidget: Text( + 'Log your days', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + image: Container( + child: SvgPicture.asset( + 'assets/images/onboard/2.svg', + semanticsLabel: 'Empty list', + width: 250, + ), + ), + bodyWidget: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check, + color: HaboColors.primary, + ), + SizedBox( + width: 10, + ), + Text( + 'Successful', + style: TextStyle(fontSize: 18), + ), + ], + ), + SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.close, + color: HaboColors.red, + ), + SizedBox( + width: 10, + ), + Text( + 'Not so successful', + style: TextStyle(fontSize: 18), + ), + ], + ), + SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.last_page, + color: HaboColors.skip, + ), + SizedBox( + width: 10, + ), + Text( + 'Skip (does not affect streaks)', + style: TextStyle(fontSize: 18), + ), + ], + ), + SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.chat_bubble_outline, + color: HaboColors.orange, + ), + SizedBox( + width: 10, + ), + Text( + 'Comment', + style: TextStyle(fontSize: 18), + ), + ], + ), + ], + ), + ), + ), + ), + PageViewModel( + title: "Observe your progress", + image: Container( + child: SvgPicture.asset( + 'assets/images/onboard/3.svg', + semanticsLabel: 'Empty list', + width: 250, + ), + ), + bodyWidget: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'You can track your progress through the calendar view in every habit or on the statistics page.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + ), + ), + ) + ]; + + @override + Widget build(BuildContext context) { + return IntroductionScreen( + pages: listPagesViewModel, + done: const Text("Done", style: TextStyle(fontWeight: FontWeight.w600)), + onDone: () { + if (Provider.of(context, listen: false).getSeenOnboarding) { + Navigator.pop(context); + } else { + Provider.of(context, listen: false).setSeenOnboarding = true; + } + }, + next: const Icon(Icons.arrow_forward), + showSkipButton: true, + skip: const Text("Skip"), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e24967e..63bc588 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,7 +1,9 @@ +import 'package:Habo/helpers.dart'; import 'package:Habo/provider.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:awesome_dialog/awesome_dialog.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:loader_overlay/loader_overlay.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -19,9 +21,36 @@ class SettingsScreen extends StatelessWidget { Provider.of(context, listen: false).setDailyNot = selectedTime; } + showRestoreDialog(BuildContext context) { + AwesomeDialog( + context: context, + dialogType: DialogType.WARNING, + headerAnimationLoop: false, + animType: AnimType.BOTTOMSLIDE, + title: "Warning", + desc: "All habits will be replaced with habits from backup.", + btnOkText: "Restore", + btnCancelText: "Cancel", + btnCancelColor: Colors.grey, + btnOkColor: HaboColors.primary, + btnCancelOnPress: () {}, + btnOkOnPress: () async { + context.loaderOverlay.show(); + await Provider.of(context, listen: false).loadBackup(); + context.loaderOverlay.hide(); + }, + )..show(); + } + @override Widget build(BuildContext context) { - return Container( + return LoaderOverlay( + useDefaultLoading: false, + overlayWidget: Center( + child: CircularProgressIndicator( + color: HaboColors.primary, + ), + ), child: Scaffold( appBar: AppBar( title: Text( @@ -55,7 +84,22 @@ class SettingsScreen extends StatelessWidget { ), ), ListTile( - title: Text("First day of the week"), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("First day of the week"), + SizedBox(width: 5), + Tooltip( + child: Icon( + Icons.info, + color: Colors.grey, + size: 18, + ), + message: + "The setting will take effect after the app restarts.", + ), + ], + ), trailing: DropdownButton( alignment: Alignment.center, items: Provider.of(context) @@ -126,6 +170,55 @@ class SettingsScreen extends StatelessWidget { }, ), ), + ListTile( + title: Text("Show month name"), + trailing: Switch( + value: Provider.of(context).getShowMonthName, + onChanged: (value) { + Provider.of(context, listen: false).setShowMonthName = + value; + }, + ), + ), + ListTile( + title: Text("Backup"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + MaterialButton( + onPressed: () async { + Provider.of(context, listen: false) + .createBackup(); + }, + child: Text( + 'Create', + style: TextStyle(decoration: TextDecoration.underline), + ), + ), + const VerticalDivider( + thickness: 1, + indent: 20, + endIndent: 20, + color: Colors.grey, + ), + MaterialButton( + onPressed: () async { + showRestoreDialog(context); + }, + child: Text( + 'Restore', + style: TextStyle(decoration: TextDecoration.underline), + ), + ), + ], + ), + ), + ListTile( + title: Text("Onboarding"), + onTap: () { + navigateToOnboarding(context); + }, + ), ListTile( title: Text("About"), onTap: () { @@ -141,7 +234,7 @@ class SettingsScreen extends StatelessWidget { Provider.of(context, listen: false) .getPackageInfo .version, - applicationLegalese: '©2021 Habo', + applicationLegalese: '©2022 Habo', children: [ Padding( padding: EdgeInsets.only(top: 15), diff --git a/lib/screens/statistics_screen.dart b/lib/screens/statistics_screen.dart index e034185..0c06e87 100644 --- a/lib/screens/statistics_screen.dart +++ b/lib/screens/statistics_screen.dart @@ -4,9 +4,7 @@ import 'package:Habo/statistics.dart'; import 'package:Habo/widgets/empty_statistics_image.dart'; import 'package:Habo/widgets/overall_statistics_card.dart'; import 'package:Habo/widgets/statistics_card.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/painting.dart'; import 'package:provider/provider.dart'; class StatisticsScreen extends StatefulWidget { diff --git a/lib/settings_data.dart b/lib/settings_data.dart index 06e1bae..4e90fa0 100644 --- a/lib/settings_data.dart +++ b/lib/settings_data.dart @@ -19,6 +19,8 @@ class SettingsData { TimeOfDay dailyNotTime = TimeOfDay(hour: 20, minute: 0); bool showDailyNot = true; bool soundEffects = true; + bool showMonthName = true; + bool seenOnboarding = false; SettingsData(); @@ -29,9 +31,13 @@ class SettingsData { (json['showDailyNot'] != null) ? json['showDailyNot'] : true, soundEffects = (json['soundEffects'] != null) ? json['soundEffects'] : true, + showMonthName = + (json['showMonthName'] != null) ? json['showMonthName'] : true, dailyNotTime = (json['notTime'] != null) ? parseTimeOfDay(json['notTime']) - : TimeOfDay(hour: 20, minute: 0); + : TimeOfDay(hour: 20, minute: 0), + seenOnboarding = + (json['seenOnboarding'] != null) ? json['seenOnboarding'] : false; ThemeData get getDark { if (theme != Themes.Light) { @@ -81,6 +87,14 @@ class SettingsData { return soundEffects; } + bool get getShowMonthName { + return showMonthName; + } + + bool get getSeenOnboarding { + return seenOnboarding; + } + set setDailyNot(TimeOfDay notTime) { dailyNotTime = notTime; } @@ -101,6 +115,14 @@ class SettingsData { soundEffects = value; } + set setShowMonthName(bool value) { + showMonthName = value; + } + + set setSeenOnboarding(bool value) { + seenOnboarding = value; + } + Map toJson() => { '"theme"': theme.index, '"weekStart"': weekStart.index, @@ -110,7 +132,9 @@ class SettingsData { dailyNotTime.minute.toString() + '"', '"showDailyNot"': showDailyNot, - '"soundEffects"': soundEffects + '"soundEffects"': soundEffects, + '"showMonthName"': showMonthName, + '"seenOnboarding"': seenOnboarding }; } diff --git a/lib/widgets/habit.dart b/lib/widgets/habit.dart index 4c5ca61..300a65e 100644 --- a/lib/widgets/habit.dart +++ b/lib/widgets/habit.dart @@ -8,7 +8,6 @@ import 'package:Habo/screens/edit_habit_screen.dart'; import 'package:Habo/widgets/habit_header.dart'; import 'package:Habo/widgets/one_day.dart'; import 'package:Habo/widgets/one_day_button.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:table_calendar/table_calendar.dart'; @@ -40,6 +39,55 @@ class Habit extends StatefulWidget { }; } + Map toJson() { + return { + "id": this.habitData.id, + "title": this.habitData.title, + "twoDayRule": this.habitData.twoDayRule ? 1 : 0, + "position": this.habitData.position, + "cue": this.habitData.cue, + "routine": this.habitData.routine, + "reward": this.habitData.reward, + "showReward": this.habitData.showReward ? 1 : 0, + "advanced": this.habitData.advanced ? 1 : 0, + "notification": this.habitData.notification ? 1 : 0, + "notTime": this.habitData.notTime.hour.toString() + + ":" + + this.habitData.notTime.minute.toString(), + "events": this.habitData.events.map((key, value) { + return MapEntry(key.toString(), [value[0].toString(), value[1]]); + }), + }; + } + + Habit.fromJson(Map json) + : habitData = HabitData( + id: json['id'], + position: json['position'], + title: json['title'], + twoDayRule: json['twoDayRule'] != 0 ? true : false, + cue: json['cue'], + routine: json['routine'], + reward: json['reward'], + showReward: json['showReward'] != 0 ? true : false, + advanced: json['advanced'] != 0 ? true : false, + notification: json['notification'] != 0 ? true : false, + notTime: parseTimeOfDay(json['notTime']), + events: doEvents(json['events']), + ); + + static SplayTreeMap doEvents(Map input) { + SplayTreeMap result = new SplayTreeMap(); + + input.forEach((key, value) { + result[DateTime.parse(key)] = [ + DayType.values.firstWhere((e) => e.toString() == value[0]), + value[1] + ]; + }); + return result; + } + @override _HabitState createState() => _HabitState(habitData); @@ -60,6 +108,8 @@ class _HabitState extends State { bool _streakVisible = false; CalendarFormat _calendarFormat = CalendarFormat.week; HabitData _habitData; + bool _showMonth = false; + String _actualMonth = ""; _HabitState(habitData) : this._habitData = habitData; @@ -108,9 +158,15 @@ class _HabitState extends State { streakVisible: _streakVisible, orangeStreak: _orangeStreak, streak: _habitData.streak), + if (_showMonth && Provider.of(context).getShowMonthName) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text(_actualMonth), + ), TableCalendar( headerVisible: false, events: _habitData.events, + calendarController: _habitData.calendarController, endDay: DateTime.now(), initialCalendarFormat: _calendarFormat, availableCalendarFormats: const { @@ -121,6 +177,22 @@ class _HabitState extends State { renderDaysOfWeek: false, contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5)), startingDayOfWeek: Provider.of(context).getWeekStartEnum, + onCalendarCreated: (start, end, format) { + _showMonth = (format == CalendarFormat.month); + var days = _habitData.calendarController.visibleDays; + _actualMonth = months[days[days.length ~/ 2].month] + + " " + + days[days.length ~/ 2].year.toString(); + }, + onVisibleDaysChanged: (start, end, format) { + setState(() { + _showMonth = (format == CalendarFormat.month); + var days = _habitData.calendarController.visibleDays; + _actualMonth = months[days[days.length ~/ 2].month] + + " " + + days[days.length ~/ 2].year.toString(); + }); + }, builders: CalendarBuilders( dayBuilder: (context, date, _) { return OneDayButton( @@ -186,7 +258,6 @@ class _HabitState extends State { return children; }, ), - calendarController: _habitData.calendarController, ) ], ), diff --git a/pubspec.lock b/pubspec.lock index 9ea58fc..53af1e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" audio_session: dependency: transitive description: @@ -21,7 +21,14 @@ packages: name: awesome_dialog url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" + back_button_interceptor: + dependency: transitive + description: + name: back_button_interceptor + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" boolean_selector: dependency: transitive description: @@ -35,7 +42,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -64,6 +71,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + dots_indicator: + dependency: transitive + description: + name: dots_indicator + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" equatable: dependency: transitive description: @@ -98,7 +112,7 @@ packages: name: fl_chart url: "https://pub.dartlang.org" source: hosted - version: "0.40.2" + version: "0.40.6" flare_flutter: dependency: transitive description: @@ -111,6 +125,20 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_file_dialog: + dependency: "direct main" + description: + name: flutter_file_dialog + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + flutter_lints: + dependency: transitive + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" flutter_local_notifications: dependency: "direct main" description: @@ -149,6 +177,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + introduction_screen: + dependency: "direct main" + description: + name: introduction_screen + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" js: dependency: transitive description: @@ -162,7 +197,7 @@ packages: name: just_audio url: "https://pub.dartlang.org" source: hosted - version: "0.9.11" + version: "0.9.18" just_audio_platform_interface: dependency: transitive description: @@ -176,14 +211,35 @@ packages: name: just_audio_web url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.3" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + loader_overlay: + dependency: "direct main" + description: + name: loader_overlay + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -218,7 +274,7 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.5.1" + version: "0.5.1+1" path_parsing: dependency: transitive description: @@ -227,103 +283,124 @@ packages: source: hosted version: "0.2.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.8" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" + version: "2.0.5" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "4.4.0" platform: dependency: transitive description: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.2" process: dependency: transitive description: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.2.3" + version: "4.2.4" provider: dependency: "direct main" description: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "6.0.2" rxdart: dependency: transitive description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.2" + version: "0.27.3" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.13" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.4" shared_preferences_macos: dependency: transitive description: @@ -344,21 +421,21 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.4" simple_animations: dependency: transitive description: name: simple_animations url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "4.0.1" simple_gesture_detector: dependency: transitive description: @@ -384,14 +461,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+4" + version: "2.0.2" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.0.1+1" + version: "2.2.0" stack_trace: dependency: transitive description: @@ -413,20 +490,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" - supercharged: - dependency: transitive - description: - name: supercharged - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - supercharged_dart: - dependency: transitive - description: - name: supercharged_dart - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" synchronized: dependency: transitive description: @@ -454,7 +517,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.8" timezone: dependency: transitive description: @@ -475,35 +538,49 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.10" + version: "6.0.18" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.14" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.14" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.6" url_launcher_windows: dependency: transitive description: @@ -517,35 +594,35 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.4" + version: "3.0.5" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.2.9" + version: "2.3.10" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.0+1" xml: dependency: transitive description: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.2.0" + version: "5.3.1" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.15.0 <3.0.0" flutter: ">=2.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8f66165..c506682 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Minimalistic habit tracker. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.6.2 +version: 1.0.0 environment: sdk: ">=2.3.0 <3.0.0" @@ -30,6 +30,10 @@ dependencies: awesome_dialog: ^2.0.0 just_audio: ^0.9.11 fl_chart: ^0.40.2 + flutter_file_dialog: ^2.3.0 + path_provider: ^2.0.7 + loader_overlay: ^2.0.5 + introduction_screen: ^2.1.0 dev_dependencies: flutter_test: @@ -58,6 +62,7 @@ flutter: assets: - assets/ - assets/images/ + - assets/images/onboard/ - assets/sounds/ # An image asset can refer to one or more resolution-specific "variants", see