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