├── .gitattributes ├── .gitignore ├── .gitmodules ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── LAPARCELA │ │ │ │ └── florae │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── ic_stat_florae.png │ │ │ ├── drawable-mdpi │ │ │ └── ic_stat_florae.png │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── ic_stat_florae.png │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_stat_florae.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── ic_stat_florae.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── raw │ │ │ └── keep.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── florae_android.iml ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── NotoSans-Bold.ttf ├── NotoSans-BoldItalic.ttf ├── NotoSans-Italic.ttf ├── NotoSans-Regular.ttf ├── card-sample-image.jpg ├── florae_avatar_1.png ├── florae_avatar_2.png ├── florae_avatar_3.png ├── florae_avatar_4.png ├── florae_avatar_5.png ├── florae_avatar_6.png ├── florae_avatar_7.png ├── florae_avatar_8.png ├── undraw_blooming_re_2kc4.svg ├── undraw_blooming_re_2kc4.svg.vec ├── undraw_different_love_a-3-rg.svg ├── undraw_fall_thyk.svg ├── undraw_fall_thyk.svg.vec ├── undraw_flowers_vx06.svg └── undraw_gardening_re_e658.svg ├── docs ├── banner.png └── privacy.html ├── florae.iml ├── l10n.yaml ├── lib ├── data │ ├── care.dart │ ├── care.g.dart │ ├── default.dart │ ├── garden.dart │ ├── plant.dart │ └── plant.g.dart ├── l10n │ ├── app_ar.arb │ ├── app_de.arb │ ├── app_en.arb │ ├── app_es.arb │ ├── app_fr.arb │ ├── app_nl.arb │ ├── app_ru.arb │ └── app_zh.arb ├── main.dart ├── notifications.dart ├── screens │ ├── care_plant.dart │ ├── error.dart │ ├── home_page.dart │ ├── manage_plant.dart │ ├── picture_viewer.dart │ └── settings.dart ├── themes │ ├── darkTheme.dart │ └── lightTheme.dart └── utils │ └── random.dart ├── metadata ├── en-US │ ├── changelogs │ │ ├── 1.txt │ │ └── 5.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ ├── short_description.txt │ └── title.txt └── es │ ├── changelogs │ ├── 1.txt │ └── 5.txt │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | 8 | # Directory created by dartdoc 9 | # If you don't generate documentation locally you can remove this line. 10 | doc/api/ 11 | 12 | # dotenv environment variables file 13 | .env* 14 | 15 | # Avoid committing generated Javascript files: 16 | *.dart.js 17 | *.info.json # Produced by the --dump-info flag. 18 | *.js # When generated by dart2js. Don't specify *.js if your 19 | # project includes source files written in JavaScript. 20 | *.js_ 21 | *.js.deps 22 | *.js.map 23 | 24 | .flutter-plugins 25 | .flutter-plugins-dependencies 26 | .idea/ 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".flutter"] 2 | path = .flutter 3 | url = https://github.com/flutter/flutter 4 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 18116933e77adc82f80866c928266a5b4f1ed645 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | Banner logo 6 | 7 | 8 |

9 | Keep care of your green, leafy best friends 10 |

11 |
12 | 13 | # About 14 | 15 | Florae is a free application that will allow you to keep track of the care of your plants while respecting your freedom and privacy. 16 | 17 | It is proven that having plants in rooms keeps you healthier and happier. They raise the air quality and humidity, absorb toxic substances and can improve productivity and concentration. 18 | 19 | Growing them and taking care of them is relatively simple but requires responsibility and being aware of the requirements of irrigation, pruning, cleaning, etc. 20 | 21 | This app allows you to manage all the care of your plants and receive notifications when they require it. 22 | 23 | ## Features 24 | 25 | * Easily manage your plants and the care they require. 26 | * Set care alerts with the desired delay time. 27 | * Consult future care for better planning. 28 | * Open source with no hidden costs. 29 | 30 | ## Release 31 | 32 | Get it on Google Play 33 | 34 | Get it on F-Droid 37 | 38 | 39 | 40 | ## Development 41 | 42 | This application is entirely built with Flutter, and its operation is only oriented to Android devices, this is due to some dependencies used for the alerts/notifications system and the restrictions in the iOS environment to issue them without proprietary dependencies. 43 | 44 | ### Notifications 45 | 46 | Notifications may not be delivered correctly or may not be delivered at all, because some mobile device manufacturers integrate very aggressive battery optimizations that do not respect the original Android APIs. This makes it impossible for developers to universally implement applications that run tasks in the background. 47 | 48 | For this case a background activity is launched with a user-configurable frequency to analyze the plant database and check if any of them require attention and issue the corresponding notification. 49 | 50 | If your device is among those affected and notifications are not displayed, please consult: [Don't kill my app!](https://dontkillmyapp.com/) 51 | 52 | ### Languages 53 | 54 | Florae is currently translated into the following languages: `English`, `Español`, `Français`, `Nederlands`, `中文`, `Русский` and `Arabic`. 55 | 56 | If you wish to contribute to Florae by adding a new language, just include the translation file in [`lib/l10n`](lib/l10n). I will be happy to accept your pull request. 57 | 58 | ## License 59 | 60 | Code is released under the GNU General Public License v3.0. 61 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | 26 | 27 | def keystoreProperties = new Properties() 28 | def keystorePropertiesFile = rootProject.file('key.properties') 29 | if (keystorePropertiesFile.exists()) { 30 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 31 | } 32 | 33 | android { 34 | compileSdkVersion rootProject.ext.compileSdkVersion 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_1_8 38 | targetCompatibility JavaVersion.VERSION_1_8 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = '1.8' 43 | } 44 | 45 | sourceSets { 46 | main.java.srcDirs += 'src/main/kotlin' 47 | } 48 | 49 | defaultConfig { 50 | applicationId "cat.naval.florae" 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion rootProject.ext.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | signingConfigs { 58 | release { 59 | keyAlias keystoreProperties['keyAlias'] 60 | keyPassword keystoreProperties['keyPassword'] 61 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 62 | storePassword keystoreProperties['storePassword'] 63 | } 64 | } 65 | 66 | buildTypes { 67 | release { 68 | signingConfig signingConfigs.release 69 | } 70 | } 71 | } 72 | 73 | flutter { 74 | source '../..' 75 | } 76 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 17 | 21 | 25 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/LAPARCELA/florae/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cat.naval.florae 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_stat_florae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/drawable-hdpi/ic_stat_florae.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_stat_florae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/drawable-mdpi/ic_stat_florae.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_stat_florae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/drawable-xhdpi/ic_stat_florae.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_stat_florae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/drawable-xxhdpi/ic_stat_florae.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_stat_florae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/drawable-xxxhdpi/ic_stat_florae.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/raw/keep.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | compileSdkVersion = 34 3 | targetSdkVersion = 34 4 | appCompatVersion = "1.4.2" 5 | } 6 | 7 | allprojects { 8 | repositories { 9 | google() 10 | mavenCentral() 11 | // for FDroid 12 | mavenLocal() 13 | maven { 14 | // [required] background_fetch 15 | url "${project(':background_fetch').projectDir}/libs" 16 | } 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | tasks.register("clean", Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /android/florae_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https://services.gradle.org/distributions/gradle-7.6-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "7.4.2" apply false 22 | id "org.jetbrains.kotlin.android" version "1.7.10" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /assets/NotoSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/NotoSans-Bold.ttf -------------------------------------------------------------------------------- /assets/NotoSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/NotoSans-BoldItalic.ttf -------------------------------------------------------------------------------- /assets/NotoSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/NotoSans-Italic.ttf -------------------------------------------------------------------------------- /assets/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /assets/card-sample-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/card-sample-image.jpg -------------------------------------------------------------------------------- /assets/florae_avatar_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/florae_avatar_1.png -------------------------------------------------------------------------------- /assets/florae_avatar_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/florae_avatar_2.png -------------------------------------------------------------------------------- /assets/florae_avatar_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/florae_avatar_3.png -------------------------------------------------------------------------------- /assets/florae_avatar_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/florae_avatar_4.png -------------------------------------------------------------------------------- /assets/florae_avatar_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/florae_avatar_5.png -------------------------------------------------------------------------------- /assets/florae_avatar_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/florae_avatar_6.png -------------------------------------------------------------------------------- /assets/florae_avatar_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/florae_avatar_7.png -------------------------------------------------------------------------------- /assets/florae_avatar_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/florae_avatar_8.png -------------------------------------------------------------------------------- /assets/undraw_blooming_re_2kc4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/undraw_blooming_re_2kc4.svg.vec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/undraw_blooming_re_2kc4.svg.vec -------------------------------------------------------------------------------- /assets/undraw_different_love_a-3-rg.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | different love 17 | 20 | 27 | 28 | 38 | 44 | 49 | 59 | 65 | 71 | 76 | 81 | 89 | 95 | 103 | 109 | 117 | 127 | 133 | 138 | 146 | 152 | 162 | 168 | 172 | 177 | 182 | 187 | 192 | 197 | 203 | 208 | 213 | 220 | 225 | 230 | 235 | 240 | 245 | 250 | 255 | 256 | -------------------------------------------------------------------------------- /assets/undraw_fall_thyk.svg: -------------------------------------------------------------------------------- 1 | fall 2 | -------------------------------------------------------------------------------- /assets/undraw_fall_thyk.svg.vec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/assets/undraw_fall_thyk.svg.vec -------------------------------------------------------------------------------- /assets/undraw_flowers_vx06.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 34 | 36 | 44 | 49 | 54 | 59 | 60 | 61 | 63 | flowers 64 | 65 | 68 | 72 | 77 | 81 | 85 | 89 | 93 | 97 | 98 | 102 | 106 | 110 | 114 | 118 | 122 | 126 | 127 | 131 | 135 | 141 | 145 | 149 | 153 | 157 | 161 | 165 | 169 | 173 | 177 | 181 | 185 | 189 | 193 | 194 | 204 | 210 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/docs/banner.png -------------------------------------------------------------------------------- /docs/privacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Privacy Policy - Florae 4 | 5 | 6 | 7 | 8 |
9 |

10 |
11 |

Privacy Policy - Florae

12 |

13 |

15 | Collection of Personal Information

16 |

The app does not collect, transmit or share any information, personal or otherwise. 17 | 18 |

19 |

Email

20 |

If you email the developer for support or other feedback, the emails with email addresses will be retained for quality assurance purposes. The email addresses will be used only to reply to the concerns or suggestions raised and will never be used for any marketing purpose.

21 |

Disclosure of Personal Information

22 |

We will not disclose your information to any third party except if you expressly consent or where required by law.

23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /florae.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /lib/data/care.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'care.g.dart'; 4 | 5 | @JsonSerializable() 6 | class Care { 7 | int id = 0; 8 | String name; 9 | int cycles = 0; 10 | DateTime? effected; 11 | 12 | Care( 13 | {required this.name, 14 | required this.cycles, 15 | required this.effected, 16 | required this.id}); 17 | 18 | factory Care.fromJson(Map json) => _$CareFromJson(json); 19 | 20 | Map toJson() => _$CareToJson(this); 21 | } 22 | -------------------------------------------------------------------------------- /lib/data/care.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'care.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Care _$CareFromJson(Map json) => Care( 10 | name: json['name'] as String, 11 | cycles: json['cycles'] as int, 12 | effected: json['effected'] == null 13 | ? null 14 | : DateTime.parse(json['effected'] as String), 15 | id: json['id'] as int, 16 | ); 17 | 18 | Map _$CareToJson(Care instance) => { 19 | 'id': instance.id, 20 | 'name': instance.name, 21 | 'cycles': instance.cycles, 22 | 'effected': instance.effected?.toIso8601String(), 23 | }; 24 | -------------------------------------------------------------------------------- /lib/data/default.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | 4 | class DefaultValues { 5 | static Map cares = {}; 6 | 7 | DefaultValues(BuildContext context) { 8 | cares["water"] = CareMap( 9 | name: "water", 10 | translatedName: AppLocalizations.of(context)!.water, 11 | description: "Water", 12 | defaultCycles: 3, 13 | color: Colors.blue, 14 | icon: Icons.opacity); 15 | cares["spray"] = CareMap( 16 | name: "spray", 17 | translatedName: AppLocalizations.of(context)!.spray, 18 | description: "Spray", 19 | defaultCycles: 0, 20 | color: Colors.lightGreen, 21 | icon: Icons.air); 22 | cares["rotate"] = CareMap( 23 | name: "rotate", 24 | translatedName: AppLocalizations.of(context)!.rotate, 25 | description: "Rotate", 26 | defaultCycles: 0, 27 | color: Colors.purple, 28 | icon: Icons.rotate_90_degrees_ccw); 29 | cares["prune"] = CareMap( 30 | name: "prune", 31 | translatedName: AppLocalizations.of(context)!.prune, 32 | description: "Prune", 33 | defaultCycles: 0, 34 | color: Colors.orange, 35 | icon: Icons.cut); 36 | cares["fertilise"] = CareMap( 37 | name: "fertilise", 38 | translatedName: AppLocalizations.of(context)!.fertilise, 39 | description: "Fertilise", 40 | defaultCycles: 0, 41 | color: Colors.brown, 42 | icon: Icons.workspaces_filled); 43 | cares["transplant"] = CareMap( 44 | name: "transplant", 45 | translatedName: AppLocalizations.of(context)!.transplant, 46 | description: "Transplant", 47 | defaultCycles: 0, 48 | color: Colors.green, 49 | icon: Icons.compost); 50 | cares["clean"] = CareMap( 51 | name: "clean", 52 | translatedName: AppLocalizations.of(context)!.clean, 53 | description: "Clean", 54 | defaultCycles: 0, 55 | color: Colors.blueGrey, 56 | icon: Icons.cleaning_services); 57 | } 58 | 59 | static CareMap? getCare(BuildContext context, String care) { 60 | DefaultValues(context); 61 | return cares[care]; 62 | } 63 | 64 | static Map getCares(BuildContext context) { 65 | DefaultValues(context); 66 | return cares; 67 | } 68 | } 69 | 70 | class CareMap { 71 | String name; 72 | String translatedName; 73 | String description; 74 | int defaultCycles; 75 | MaterialColor color; 76 | IconData icon; 77 | 78 | CareMap( 79 | {required this.name, 80 | required this.translatedName, 81 | required this.description, 82 | required this.defaultCycles, 83 | required this.color, 84 | required this.icon}); 85 | } 86 | -------------------------------------------------------------------------------- /lib/data/garden.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:florae/data/plant.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | class Garden { 6 | late final SharedPreferences store; 7 | 8 | Garden(this.store); 9 | 10 | static Future load() async { 11 | var store = await SharedPreferences.getInstance(); 12 | 13 | await store.reload(); 14 | 15 | return (Garden(store)); 16 | } 17 | 18 | Future> getAllPlants() async { 19 | List allPlants = []; 20 | var rawPlants = store.getString("plants"); 21 | if (rawPlants != null) { 22 | Iterable l = json.decode(rawPlants); 23 | allPlants = List.from(l.map((model) => Plant.fromJson(model))); 24 | } 25 | return allPlants; 26 | } 27 | 28 | // Returns true if update 29 | // Return false if create 30 | Future addOrUpdatePlant(Plant plant) async { 31 | List allPlants = await getAllPlants(); 32 | bool status; 33 | 34 | var plantIndex = allPlants.indexWhere((element) => element.id == plant.id); 35 | if (plantIndex == -1) { 36 | allPlants.add(plant); 37 | status = false; 38 | } else { 39 | allPlants[plantIndex] = plant; 40 | status = true; 41 | } 42 | String jsonPlants = jsonEncode(allPlants); 43 | await store.setString("plants", jsonPlants); 44 | 45 | return status; 46 | } 47 | 48 | Future deletePlant(Plant plant) async { 49 | List allPlants = await getAllPlants(); 50 | 51 | var plantIndex = allPlants.indexWhere((element) => element.id == plant.id); 52 | if (plantIndex != -1) { 53 | allPlants.removeAt(plantIndex); 54 | 55 | String jsonPlants = jsonEncode(allPlants); 56 | await store.setString("plants", jsonPlants); 57 | 58 | return true; 59 | } else { 60 | return false; 61 | } 62 | } 63 | 64 | Future updatePlant(Plant plant) async { 65 | List allPlants = await getAllPlants(); 66 | 67 | var plantIndex = allPlants.indexWhere((element) => element.id == plant.id); 68 | if (plantIndex != -1) { 69 | allPlants[plantIndex] = plant; 70 | 71 | String jsonPlants = jsonEncode(allPlants); 72 | await store.setString("plants", jsonPlants); 73 | return true; 74 | } else { 75 | return false; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/data/plant.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'care.dart'; 3 | 4 | part 'plant.g.dart'; 5 | 6 | @JsonSerializable(explicitToJson: true) 7 | class Plant { 8 | int id = 0; 9 | String name; 10 | String? location; 11 | String description; 12 | DateTime createdAt; 13 | String? picture; 14 | List cares = []; 15 | 16 | Plant( 17 | {required this.name, 18 | this.id = 0, 19 | this.location, 20 | this.description = "", 21 | required this.createdAt, 22 | this.picture, 23 | required this.cares}); 24 | 25 | factory Plant.fromJson(Map json) => _$PlantFromJson(json); 26 | 27 | Map toJson() => _$PlantToJson(this); 28 | } 29 | -------------------------------------------------------------------------------- /lib/data/plant.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'plant.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Plant _$PlantFromJson(Map json) => Plant( 10 | name: json['name'] as String, 11 | id: json['id'] as int? ?? 0, 12 | location: json['location'] as String?, 13 | description: json['description'] as String? ?? "", 14 | createdAt: DateTime.parse(json['createdAt'] as String), 15 | picture: json['picture'] as String?, 16 | cares: (json['cares'] as List) 17 | .map((e) => Care.fromJson(e as Map)) 18 | .toList(), 19 | ); 20 | 21 | Map _$PlantToJson(Plant instance) => { 22 | 'id': instance.id, 23 | 'name': instance.name, 24 | 'location': instance.location, 25 | 'description': instance.description, 26 | 'createdAt': instance.createdAt.toIso8601String(), 27 | 'picture': instance.picture, 28 | 'cares': instance.cares.map((e) => e.toJson()).toList(), 29 | }; 30 | -------------------------------------------------------------------------------- /lib/l10n/app_ar.arb: -------------------------------------------------------------------------------- 1 | { 2 | "no": "لا", 3 | "yes": "نعم", 4 | "mainNoCares": "عظيم! ليس هناك نباتات بحاجة الى رعاية", 5 | "mainNoPlants": "الحديقة فارغة، هلَّا غرسنا غرساً؟", 6 | "buttonGarden": "الحديقة", 7 | "buttonToday": "اليوم", 8 | "tooltipCareAll": "اعتنيت بكل النباتات لهذا اليوم.", 9 | "tooltipShowCalendar": "اعرض التقويم", 10 | "tooltipSettings": "الإعدادات", 11 | "tooltipNewPlant": "إضافة غرس جديد", 12 | "selectDays": "إختر الأيام", 13 | "ok": "حسناً", 14 | "every": "كل", 15 | "days": "أيام", 16 | "never": "أبداً", 17 | "titleEditPlant": "تعديل", 18 | "titleNewPlant": "جديد", 19 | "tooltipCameraImage": "صورة من الكاميرا", 20 | "tooltipNextAvatar": "الأفاتر التالي ", 21 | "tooltipGalleryImage": "صورة من المعرض", 22 | "emptyError": "رجاءً أدخل نص", 23 | "conflictError": "إسم النبتة موجود مسبقاً", 24 | "labelName": "الاسم", 25 | "exampleName": "مثلاً: بيرحاء", 26 | "labelDescription": "الوصف", 27 | "labelLocation": "المكان", 28 | "exampleLocation": "مثال: الباحة الخلفية ", 29 | "labelDayPlanted": "يوم الغرس", 30 | "saveButton": "حفظ", 31 | "careAll": "هل اعتنيت بجميع النباتات", 32 | "careAllBody": "سيعين هذا الأمر أنك اعتنيت بجميع النباتات لهذا اليوم", 33 | "water": "سقاية", 34 | "spray": "رش", 35 | "rotate": "تقليب", 36 | "prune": "تقليم", 37 | "fertilise": "تسميد", 38 | "transplant": "نقل", 39 | "clean": "تنظيف", 40 | "now": "الآن", 41 | "daysLeft": "أيام متبقية", 42 | "daysLate": "منذ", 43 | "tooltipEdit": "تعديل هذه النبتة", 44 | "deleteButton": "حذف", 45 | "noCaresError": "اختر مهمة عناية واحدة على الأقل", 46 | "careButton": "إعتناء", 47 | "selectHours": "اختر الساعات", 48 | "notifyEvery": "إشعار كل", 49 | "hours": "ساعات", 50 | "testNotificationButton": "تجربة الإشعارات", 51 | "testNotificationTitle": "إشعار تجريبي لتطبيق فلوري", 52 | "testNotificationBody": "هذا نص تجريبي", 53 | "aboutFloraeButton": "عن فلوراي", 54 | "notificationInfo": "سيتم إعادة ضبط وقت الإشعارات عند دخولك للتطبيق مجدداً.\n\n مع العلم بأن بعض الأجهزة تطبق سياسة توفير طاقة شديدة قد تؤدي إلى عدم إصدار الإشعارات بشكل صحيح.", 55 | "careNotificationTitle": "نباتات بحاجة إلى عناية", 56 | "careNotificationName": "تذكير العناية", 57 | "careNotificationDescription": "استلم إشعارات العناية عند حاجة نباتاتك إلى إهتمام", 58 | "deletePlantTitle": "حذف الغرس", 59 | 60 | "deletePlantBody": "أنت على وشك حذف نباتك بشكل نهائي، هذا الفعل لا عودة عنه.", 61 | "@helloWorld": { 62 | "description": "تحية المبرمجين الجدد المتعارف عليها" 63 | } 64 | } -------------------------------------------------------------------------------- /lib/l10n/app_de.arb: -------------------------------------------------------------------------------- 1 | { 2 | "no": "Nein", 3 | "yes": "Ja", 4 | "mainNoCares": "Juhu! Sie haben keine ausstehenden Pflanzen, um die Sie sich kümmern müssen.", 5 | "mainNoPlants": "Der Garten ist leer, sollen wir etwas pflanzen?", 6 | "buttonGarden": "Garten", 7 | "buttonToday": "Heute", 8 | "tooltipCareAll": "Pflege für alle Pflanzen", 9 | "tooltipShowCalendar": "Kalender anzeigen", 10 | "tooltipSettings": "Einstellungen", 11 | "tooltipNewPlant": "Neue Pflanze hinzufügen", 12 | "selectDays": "Tage auswählen", 13 | "ok": "OK", 14 | "every": "jeden", 15 | "days": "Tage", 16 | "never": "nie", 17 | "titleEditPlant": "bearbeiten", 18 | "titleNewPlant": "neu", 19 | "tooltipCameraImage": "Bild von der Kamera abrufen", 20 | "tooltipNextAvatar": "Nächster Avatar", 21 | "tooltipGalleryImage": "Bild aus der Galerie holen", 22 | "emptyError": "Bitte geben Sie einen Text ein", 23 | "conflictError": "Pflanzenname existiert bereits", 24 | "labelName": "Name", 25 | "exampleName": "Bsp: Pilea", 26 | "labelDescription": "Beschreibung", 27 | "labelLocation": "Standort", 28 | "exampleLocation": "Bsp: Innenhof", 29 | "labelDayPlanted": "Tag gepflanzt", 30 | "saveButton": "Speichern", 31 | "careAll": "Haben Sie sich um alle Pflanzen gekümmert?", 32 | "careAllBody": "Mit dieser Aktion werden alle Pflanzen als für den heutigen Zyklus gepflegt markiert.", 33 | "water": "Wasser", 34 | "spray": "Spray", 35 | "rotate": "drehen", 36 | "prune": "Pflaume", 37 | "fertilise": "düngen", 38 | "transplant": "Transplantation", 39 | "clean": "sauber", 40 | "now": "jezt", 41 | "daysLeft": "Tage frei", 42 | "daysLate": "Seit", 43 | "tooltipEdit": "Diese Pflanze bearbeiten", 44 | "deleteButton": "löschen", 45 | "noCaresError": "Wählen Sie mindestens eine Pflege", 46 | "careButton": "Pflege", 47 | "selectHours": "Stunden auswählen", 48 | "notifyEvery": "Benachrichtigen Sie jeden", 49 | "hours": "Stunden", 50 | "testNotificationButton": "Test-Benachrichtigung", 51 | "testNotificationTitle": "Florae Test Benachrichtigung", 52 | "testNotificationBody": "Dies ist eine Testmeldung", 53 | "aboutFloraeButton": "Über Florae", 54 | "notificationInfo": "Die Benachrichtigungszeit wird zurückgesetzt, wenn Sie die App aufrufen.\n\nBitte beachten Sie, dass einige Geräte sehr aggressive Batterieoptimierungen durchführen, die dazu führen können, dass Benachrichtigungen nicht korrekt ausgegeben werden.", 55 | "careNotificationTitle": "Pflanzen brauchen Pflege", 56 | "careNotificationName": "Pflegehinweise", 57 | "careNotificationDescription": "Benachrichtigungen zur Pflanzenpflege erhalten", 58 | "deletePlantTitle": "Pflanze löschen", 59 | "deletePlantBody": "Sie werden Ihre Pflanze nun endgültig beseitigen, diese Aktion kann nicht mehr rückgängig gemacht werden.", 60 | "@helloWorld": { 61 | "description": "Die herkömmliche Begrüßung eines neugeborenen Programmierers" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "no": "No", 3 | "yes": "Yes", 4 | "mainNoCares": "Yay! You don't have any pending plants to care", 5 | "mainNoPlants": "The garden is empty, shall we plant something?", 6 | "buttonGarden": "Garden", 7 | "buttonToday": "Today", 8 | "tooltipCareAll": "Apply care to all plants", 9 | "tooltipShowCalendar": "Show Calendar", 10 | "tooltipSettings": "Settings", 11 | "tooltipNewPlant": "Add new plant", 12 | "selectDays": "Select days", 13 | "ok": "OK", 14 | "every": "every", 15 | "days": "days", 16 | "never": "Never", 17 | "titleEditPlant": "Edit", 18 | "titleNewPlant": "New", 19 | "tooltipCameraImage": "Get image from camera", 20 | "tooltipNextAvatar": "Next avatar", 21 | "tooltipGalleryImage": "Get image from gallery", 22 | "emptyError": "Please enter some text", 23 | "conflictError": "Plant name already exists", 24 | "labelName": "Name", 25 | "exampleName": "Ex: Pilea", 26 | "labelDescription": "Description", 27 | "labelLocation": "Location", 28 | "exampleLocation": "Ex: Courtyard", 29 | "labelDayPlanted": "Day planted", 30 | "saveButton": "Save", 31 | "careAll": "Have you taken care of all the plants?", 32 | "careAllBody": "This action will mark all plants as cared for today cycle.", 33 | "water": "Water", 34 | "spray": "Spray", 35 | "rotate": "Rotate", 36 | "prune": "Prune", 37 | "fertilise": "Fertilise", 38 | "transplant": "Transplant", 39 | "clean": "Clean", 40 | "now": "Now", 41 | "daysLeft": "days left", 42 | "daysLate": "Since", 43 | "tooltipEdit": "Edit this plant", 44 | "deleteButton": "Delete", 45 | "noCaresError": "Select at least one care", 46 | "careButton": "Care", 47 | "selectHours": "Select hours", 48 | "notifyEvery": "Notify every", 49 | "hours": "hours", 50 | "testNotificationButton": "Test notification", 51 | "testNotificationTitle": "Florae Test Notification", 52 | "testNotificationBody": "This is a test message", 53 | "aboutFloraeButton": "About Florae", 54 | "notificationInfo": "The notification time will be reset when you enter the App.\n\nPlease note that some devices perform very aggressive battery optimizations that may cause notifications to not be issued correctly.", 55 | "careNotificationTitle": "Plants require care", 56 | "careNotificationName": "Care reminder", 57 | "careNotificationDescription": "Receive plants care notifications", 58 | "deletePlantTitle": "Delete plant", 59 | "deletePlantBody": "You are going to proceed to eliminate your plant definitively, this action cannot be undone.", 60 | "@helloWorld": { 61 | "description": "The conventional newborn programmer greeting" 62 | } 63 | } -------------------------------------------------------------------------------- /lib/l10n/app_es.arb: -------------------------------------------------------------------------------- 1 | { 2 | "no": "No", 3 | "yes": "Sí", 4 | "mainNoCares": "¡Genial! No hay plantas que necesiten atención", 5 | "mainNoPlants": "El jardín está vacío, ¿plantamos algo?", 6 | "buttonGarden": "Jardín", 7 | "buttonToday": "Hoy", 8 | "tooltipCareAll": "Cuidar todas las plantas", 9 | "tooltipShowCalendar": "Mostrar calendario", 10 | "tooltipSettings": "Ajustes", 11 | "tooltipNewPlant": "Añadir una nueva planta", 12 | "selectDays": "Número de días", 13 | "ok": "Aceptar", 14 | "every": "cada", 15 | "days": "días", 16 | "never": "Nunca", 17 | "titleEditPlant": "Editar", 18 | "titleNewPlant": "Nueva", 19 | "tooltipCameraImage": "Obtener la imagen desde la cámara", 20 | "tooltipNextAvatar": "Siguiente avatar", 21 | "tooltipGalleryImage": "Obtener la imagen desde la galería", 22 | "emptyError": "El nombre no puede estar vacío", 23 | "conflictError": "El nombre de la planta ya existe", 24 | "labelName": "Nombre", 25 | "exampleName": "por ej. Pilea", 26 | "labelDescription": "Descripción", 27 | "labelLocation": "Ubicación", 28 | "exampleLocation": "por ej. Patio", 29 | "labelDayPlanted": "Plantada el día", 30 | "saveButton": "Guardar", 31 | "careAll": "¿Has atendido a todas tus plantas?", 32 | "careAllBody": "Esta acción reiniciará los cuidados de todas las plantas que requieren atención.", 33 | "water": "Regar", 34 | "spray": "Pulverizar", 35 | "rotate": "Girar", 36 | "prune": "Podar", 37 | "fertilise": "Fertilizar", 38 | "transplant": "Trasplantar", 39 | "clean": "Limpiar", 40 | "now": "Ahora", 41 | "daysLeft": "días restantes", 42 | "daysLate": "Desde hace", 43 | "tooltipEdit": "Editar esta planta", 44 | "deleteButton": "Borrar", 45 | "noCaresError": "Selecciona al menos un tipo de cuidado", 46 | "careButton": "Cuidar", 47 | "selectHours": "Selecciona horas", 48 | "notifyEvery": "Notificar cada", 49 | "hours": "horas", 50 | "testNotificationButton": "Notificación de prueba", 51 | "testNotificationTitle": "Notificación de prueba de Florae", 52 | "testNotificationBody": "Esto es un mensaje de prueba", 53 | "aboutFloraeButton": "Sobre Florae", 54 | "notificationInfo": "El periodo entre notificaciones se restablecerá cuando accedas a la aplicación.\n\nTen en cuenta que algunos fabricantes aplican optimizaciones de batería muy agresivas que pueden provocar que las notificaciones no funcionen adecuadamente.", 55 | "careNotificationTitle": "Hay plantas que requieren atención", 56 | "careNotificationName": "Alertas de cuidados", 57 | "careNotificationDescription": "Recibe notificaciones cuando tus plantas te necesiten", 58 | "deletePlantTitle": "Borrar planta", 59 | "deletePlantBody": "Esta acción eliminará la planta de forma definitiva. ¿Deseas hacerlo?" 60 | } -------------------------------------------------------------------------------- /lib/l10n/app_fr.arb: -------------------------------------------------------------------------------- 1 | { 2 | "no": "Non", 3 | "yes": "Oui", 4 | "mainNoCares": "Youpi ! Vous n'avez aucune plante en attente de soin", 5 | "mainNoPlants": "Le jardin est vide, pourquoi ne pas planter quelque chose ?", 6 | "buttonGarden": "Jardin", 7 | "buttonToday": "Aujourd'hui", 8 | "tooltipCareAll": "Marquer toutes les plantes comme soignées", 9 | "tooltipShowCalendar": "Afficher le calendrier", 10 | "tooltipSettings": "Paramètres", 11 | "tooltipNewPlant": "Ajouter une plante", 12 | "selectDays": "Sélectionner les jours", 13 | "ok": "OK", 14 | "every": "tous les", 15 | "days": "jours", 16 | "never": "Jamais", 17 | "titleEditPlant": "Modifier", 18 | "titleNewPlant": "Nouveau", 19 | "tooltipCameraImage": "Prendre une photo", 20 | "tooltipNextAvatar": "Avatar suivant", 21 | "tooltipGalleryImage": "Sélectionner dans la galerie", 22 | "emptyError": "Ajouter du texte", 23 | "conflictError": "Nom de plante déjà utilisé", 24 | "labelName": "Nom", 25 | "exampleName": "Ex: Pilea", 26 | "labelDescription": "Description", 27 | "labelLocation": "Emplacement", 28 | "exampleLocation": "Ex: Cour", 29 | "labelDayPlanted": "Jour de plante", 30 | "saveButton": "Sauvegarder", 31 | "careAll": "Avez-vous pris soin de toutes vos plantes ?", 32 | "careAllBody": "Cette action marquera toutes vos plantes comme soignées pour aujourd'hui", 33 | "water": "Arroser", 34 | "spray": "Pulvériser", 35 | "rotate": "Tourner", 36 | "prune": "Élaguer", 37 | "fertilise": "Fertiliser", 38 | "transplant": "Transplanter", 39 | "clean": "Nettoyer", 40 | "now": "Maintenant", 41 | "daysLeft": "jours restants", 42 | "daysLate": "Il y a", 43 | "tooltipEdit": "Modifier cette plante", 44 | "deleteButton": "Supprimer", 45 | "noCaresError": "Sélectionner au moins un soin", 46 | "careButton": "Soigner", 47 | "selectHours": "Sélectionner les heures", 48 | "notifyEvery": "Notifier toutes les", 49 | "hours": "heures", 50 | "testNotificationButton": "Tester les notifications", 51 | "testNotificationTitle": "Test de notification pour Florae", 52 | "testNotificationBody": "Ceci est un message test", 53 | "aboutFloraeButton": "À propos de Florae", 54 | "notificationInfo": "Le temps de notiification sera remis à zéro à l'ouverture de l'application.\n\nNotez que certains appareils sont programmés pour des optimisations de batterie aggressives, qui peuvent influencer le compprtement des notifications", 55 | "careNotificationTitle": "Vos plantes ont besoin de soin", 56 | "careNotificationName": "Rappel soins", 57 | "careNotificationDescription": "Recevoir des notifications pour le soin des plantes", 58 | "deletePlantTitle": "Supprimer plante", 59 | "deletePlantBody": "La suppression d'une plante est définitive. Cette action est irréversible", 60 | "@helloWorld": { 61 | "description": "La formule de politesse de tout programmeur" 62 | } 63 | } -------------------------------------------------------------------------------- /lib/l10n/app_nl.arb: -------------------------------------------------------------------------------- 1 | { 2 | "no": "Ne", 3 | "yes": "Ja", 4 | "mainNoCares": "Hoera! Je hebt geen planten die wachten op verzorging", 5 | "mainNoPlants": "De tuin is leeg, zullen we iets gaan planten?", 6 | "buttonGarden": "Tuin", 7 | "buttonToday": "Vandaag", 8 | "tooltipCareAll": "Verzorg alle planten", 9 | "tooltipShowCalendar": "Toon kalender", 10 | "tooltipSettings": "Instellingen", 11 | "tooltipNewPlant": "Nieuwe plant", 12 | "selectDays": "Selecteer dagen", 13 | "ok": "OK", 14 | "every": "alle", 15 | "days": "dagen", 16 | "never": "Nooit", 17 | "titleEditPlant": "Bewerk", 18 | "titleNewPlant": "Nieuw", 19 | "tooltipCameraImage": "Maak een foto", 20 | "tooltipNextAvatar": "Volgende avatar", 21 | "tooltipGalleryImage": "Kies een foto uit de gallerij", 22 | "emptyError": "Vul hier een tekst in", 23 | "conflictError": "Er is al een plant met deze naam", 24 | "labelName": "Naam", 25 | "exampleName": "B.v.: Pilea", 26 | "labelDescription": "Beschrijving", 27 | "labelLocation": "Locatie", 28 | "exampleLocation": "B.v.: binnenplaats", 29 | "labelDayPlanted": "Datum van planten", 30 | "saveButton": "Opslaan", 31 | "careAll": "Heb je alle planten verzorgd?", 32 | "careAllBody": "Deze actie markeert alle planten als verzorgd voor de cyclus van vandaag.", 33 | "water": "Water", 34 | "spray": "Sproeien", 35 | "rotate": "Draaien", 36 | "prune": "Snoeien", 37 | "fertilise": "Bemesten", 38 | "transplant": "Verplanten", 39 | "clean": "Schoonmaken", 40 | "now": "Nu", 41 | "daysLeft": "dagen over", 42 | "daysLate": "Sinds", 43 | "tooltipEdit": "Bewerk deze plant", 44 | "deleteButton": "Verwijderen", 45 | "noCaresError": "Selecteer minstens één verzorging", 46 | "careButton": "Verzorgen", 47 | "selectHours": "Selecteer uren", 48 | "notifyEvery": "Herinner mij elke", 49 | "hours": "uren", 50 | "testNotificationButton": "Test notificatie", 51 | "testNotificationTitle": "Florae Test Notificatie", 52 | "testNotificationBody": "Dit is een test notificatie van Florae", 53 | "aboutFloraeButton": "Over Florae", 54 | "notificationInfo": "De notificatie tijd zal gereset worden wanneer je de App opent.\n\nLet er op dat sommige apparaten erg aggresieve batterij optimisaties doen, die er voor kunnen zorgen dat notificaties niet aankomen.", 55 | "careNotificationTitle": "Planten die verzorging nodig hebben", 56 | "careNotificationName": "Verzorgings herinnering", 57 | "careNotificationDescription": "Ontvang plant verzorging notificaties", 58 | "deletePlantTitle": "Verwijder plant", 59 | "deletePlantBody": "Je gaat je plant definitief elimineren, deze actie kan niet ongedaan worden gemaakt.", 60 | "@helloWorld": { 61 | "description": "De conventionele groet voor nieuwe programmeurs" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/l10n/app_ru.arb: -------------------------------------------------------------------------------- 1 | { 2 | "no": "Нет", 3 | "yes": "Да", 4 | "mainNoCares": "Ура! У вас нет растений, за которыми нужно ухаживать", 5 | "mainNoPlants": "Сад пуст. Может, стоит что-нибудь посадить?", 6 | "buttonGarden": "Сад", 7 | "buttonToday": "Сегодня", 8 | "tooltipCareAll": "Отметить уход за всеми растениями", 9 | "tooltipShowCalendar": "Показать Календарь", 10 | "tooltipSettings": "Настройки", 11 | "tooltipNewPlant": "Добавить новое растение", 12 | "selectDays": "Выбрать дни", 13 | "ok": "ОК", 14 | "every": "каждые", 15 | "days": "дня (-ей)", 16 | "never": "Никогда", 17 | "titleEditPlant": "Изменить", 18 | "titleNewPlant": "Новое", 19 | "tooltipCameraImage": "Снять изображение на камеру", 20 | "tooltipNextAvatar": "Следующий аватар", 21 | "tooltipGalleryImage": "Выбрать изображение из галереи", 22 | "emptyError": "Пожалуйста, введите какой-нибудь текст", 23 | "conflictError": "Уже есть растение с таким именем", 24 | "labelName": "Имя", 25 | "exampleName": "Прим: Пилея", 26 | "labelDescription": "Описание", 27 | "labelLocation": "Место", 28 | "exampleLocation": "Прим: Двор", 29 | "labelDayPlanted": "День посадки", 30 | "saveButton": "Сохранить", 31 | "careAll": "Вы позаботились обо всех растениях?", 32 | "careAllBody": "Это отметит уход за всем растениями на сегодняшний день.", 33 | "water": "Полив", 34 | "spray": "Опрыскивание", 35 | "rotate": "Разворот", 36 | "prune": "Подрезка", 37 | "fertilise": "Удобрение", 38 | "transplant": "Пересадка", 39 | "clean": "Очистка", 40 | "now": "Сейчас", 41 | "daysLeft": "дней осталсь", 42 | "daysLate": "Начиная с", 43 | "tooltipEdit": "Изменить это растение", 44 | "deleteButton": "Удалить", 45 | "noCaresError": "Выберите хотя бы один уход", 46 | "careButton": "Уход", 47 | "selectHours": "Выбрать часы", 48 | "notifyEvery": "Напоминать каждые", 49 | "hours": "часа (-ов)", 50 | "testNotificationButton": "Тестовое уведомление", 51 | "testNotificationTitle": "Тестовое уведомление Florae", 52 | "testNotificationBody": "Это тестовое уведомление", 53 | "aboutFloraeButton": "О Florae", 54 | "notificationInfo": "Время уведомление будет сброшено, когда вы откроете Приложение.\n\nПожалуйста, имейте ввиду, что некоторые устройства могут вести очень агрессивную оптимизацию батареи. Эта оптимизация может вызывать проблемы с отображением уведомлений.", 55 | "careNotificationTitle": "Растениям нужен уход", 56 | "careNotificationName": "Напоминание об уходе", 57 | "careNotificationDescription": "Получать уведомление об уходе", 58 | "deletePlantTitle": "Удалить растение", 59 | "deletePlantBody": "Вы собираетесь полностью удалить своё растение. Это действие не может быть отменено.", 60 | "@helloWorld": { 61 | "description": "Традиционное приветствие программиста новичка." 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/l10n/app_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "no": "否", 3 | "yes": "是", 4 | "mainNoCares": "哎呀!还没有什么植物需要养护哟", 5 | "mainNoPlants": "花园是空的,种点什么吗?", 6 | "buttonGarden": "花园", 7 | "buttonToday": "今天", 8 | "tooltipCareAll": "养护所有植物", 9 | "tooltipShowCalendar": "显示日历", 10 | "tooltipSettings": "设置", 11 | "tooltipNewPlant": "新增植物", 12 | "selectDays": "选择日期", 13 | "ok": "确定", 14 | "every": "每", 15 | "days": "天", 16 | "never": "从不", 17 | "titleEditPlant": "编辑", 18 | "titleNewPlant": "新增", 19 | "tooltipCameraImage": "相机拍照", 20 | "tooltipNextAvatar": "换个头像", 21 | "tooltipGalleryImage": "相册选图", 22 | "emptyError": "请输入文字", 23 | "conflictError": "同名植物已经存在", 24 | "labelName": "名称", 25 | "exampleName": "例如:鸢尾", 26 | "labelDescription": "详情", 27 | "labelLocation": "位置", 28 | "exampleLocation": "例如:庭院", 29 | "labelDayPlanted": "种植日期", 30 | "saveButton": "保存", 31 | "careAll": "养护所有植物吗?", 32 | "careAllBody": "此操作会将所有植物标记为“本周期已养护”。", 33 | "water": "浇水", 34 | "spray": "喷水", 35 | "rotate": "转向", 36 | "prune": "修剪", 37 | "fertilise": "施肥", 38 | "transplant": "移植", 39 | "clean": "清理", 40 | "now": "现在", 41 | "daysLeft": "天到期", 42 | "daysLate": "从", 43 | "tooltipEdit": "编辑此植物", 44 | "deleteButton": "删除", 45 | "noCaresError": "请至少选择一种养护项目", 46 | "careButton": "养护", 47 | "selectHours": "选择小时", 48 | "notifyEvery": "提醒,每", 49 | "hours": "小时", 50 | "testNotificationButton": "测试提醒", 51 | "testNotificationTitle": "Florae测试提醒", 52 | "testNotificationBody": "这是一条测试信息", 53 | "aboutFloraeButton": "关于Florae", 54 | "notificationInfo": "输入后,提醒时间将会重新开始计算。\n\n请注意,有些设备进行了非常激进的电池优化,可能会导致无法正确发出提醒。", 55 | "careNotificationTitle": "植物需要养护", 56 | "careNotificationName": "养护提醒", 57 | "careNotificationDescription": "接收植物养护提醒", 58 | "deletePlantTitle": "删除植物", 59 | "deletePlantBody": "您将彻底删除此植物,此操作无法撤销。", 60 | "@helloWorld": { 61 | "description": "传统的新生儿程序员问候语" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:background_fetch/background_fetch.dart'; 3 | import 'package:florae/screens/error.dart'; 4 | import 'package:florae/themes/darkTheme.dart'; 5 | import 'package:florae/themes/lightTheme.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'data/care.dart'; 8 | import 'data/plant.dart'; 9 | import 'data/garden.dart'; 10 | import 'screens/home_page.dart'; 11 | import 'package:florae/notifications.dart' as notify; 12 | import 'package:flutter_localizations/flutter_localizations.dart'; 13 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 14 | import 'package:shared_preferences/shared_preferences.dart'; 15 | 16 | late Garden garden; 17 | 18 | Future main() async { 19 | WidgetsFlutterBinding.ensureInitialized(); 20 | 21 | garden = await Garden.load(); 22 | 23 | // Set default locale for background service 24 | final prefs = await SharedPreferences.getInstance(); 25 | String? locale = Platform.localeName.substring(0, 2); 26 | await prefs.setString('locale', locale); 27 | 28 | runApp(const FloraeApp()); 29 | 30 | BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask); 31 | } 32 | 33 | /// This "Headless Task" is run when app is terminated. 34 | @pragma('vm:entry-point') 35 | void backgroundFetchHeadlessTask(HeadlessTask task) async { 36 | var taskId = task.taskId; 37 | var timeout = task.timeout; 38 | if (timeout) { 39 | print("[BackgroundFetch] Headless task timed-out: $taskId"); 40 | BackgroundFetch.finish(taskId); 41 | return; 42 | } 43 | 44 | print("[BackgroundFetch] Headless event received: $taskId"); 45 | 46 | Garden gr = await Garden.load(); 47 | 48 | List allPlants = await gr.getAllPlants(); 49 | 50 | List plants = []; 51 | String notificationTitle = "Plants require care"; 52 | 53 | for (Plant p in allPlants) { 54 | for (Care c in p.cares) { 55 | var daysSinceLastCare = DateTime.now().difference(c.effected!).inDays; 56 | print( 57 | "headless florae plant ${p.name} with days since last care $daysSinceLastCare"); 58 | // Report all unattended care, current and past 59 | if (daysSinceLastCare != 0 && daysSinceLastCare / c.cycles >= 1) { 60 | plants.add(p.name); 61 | break; 62 | } 63 | } 64 | } 65 | 66 | try { 67 | final prefs = await SharedPreferences.getInstance(); 68 | 69 | final String locale = prefs.getString('locale') ?? "en"; 70 | 71 | if (AppLocalizations.delegate.isSupported(Locale(locale))) { 72 | final t = await AppLocalizations.delegate.load(Locale(locale)); 73 | notificationTitle = t.careNotificationTitle; 74 | } else { 75 | print("handless florae: unsupported locale " + locale); 76 | } 77 | } on Exception catch (_) { 78 | print("handless florae: Failed to load locale"); 79 | } 80 | 81 | if (plants.isNotEmpty) { 82 | notify.singleNotification(notificationTitle, plants.join('\n'), 7); 83 | print("headless florae detected plants " + plants.join(' ')); 84 | } else { 85 | print("headless florae no plants require care"); 86 | } 87 | 88 | print("[BackgroundFetch] Headless event finished: $taskId"); 89 | 90 | BackgroundFetch.finish(taskId); 91 | } 92 | 93 | class FloraeApp extends StatelessWidget { 94 | const FloraeApp({Key? key}) : super(key: key); 95 | 96 | // This widget is the root of your application. 97 | @override 98 | Widget build(BuildContext context) { 99 | return MaterialApp( 100 | title: 'Florae', 101 | localizationsDelegates: const [ 102 | AppLocalizations.delegate, // Add this line 103 | GlobalMaterialLocalizations.delegate, 104 | GlobalWidgetsLocalizations.delegate, 105 | GlobalCupertinoLocalizations.delegate, 106 | ], 107 | builder: (BuildContext context, Widget? widget) { 108 | ErrorWidget.builder = (FlutterErrorDetails errorDetails) { 109 | return ErrorPage(errorDetails: errorDetails); 110 | }; 111 | 112 | return widget!; 113 | }, 114 | supportedLocales: const [ 115 | Locale('en'), // English 116 | Locale('es'), // Spanish 117 | Locale('fr'), // French 118 | Locale('nl'), // Dutch 119 | Locale('zh'), // Chinese (Simplified, People's Republic of China) 120 | Locale('ru'), // Russian 121 | Locale('ar'), // Arabic 122 | ], 123 | theme: buildLightThemeData(), 124 | darkTheme: buildDarkThemeData(), 125 | home: const MyHomePage(title: 'Today')); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/notifications.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 2 | 3 | import 'dart:async'; 4 | 5 | final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 6 | FlutterLocalNotificationsPlugin(); 7 | 8 | void _requestPermissions() { 9 | flutterLocalNotificationsPlugin 10 | .resolvePlatformSpecificImplementation< 11 | AndroidFlutterLocalNotificationsPlugin>() 12 | ?.requestNotificationsPermission(); 13 | } 14 | 15 | Future _createNotificationChannel( 16 | String channelId, String channelName, String channelDescription) async { 17 | AndroidNotificationChannel androidNotificationChannel = 18 | AndroidNotificationChannel( 19 | channelId, 20 | channelName, 21 | description: channelDescription, 22 | ); 23 | await flutterLocalNotificationsPlugin 24 | .resolvePlatformSpecificImplementation< 25 | AndroidFlutterLocalNotificationsPlugin>() 26 | ?.createNotificationChannel(androidNotificationChannel); 27 | } 28 | 29 | void initNotifications(String channelName, String channelDescription) async { 30 | _requestPermissions(); 31 | _createNotificationChannel("care_reminder", channelName, channelDescription); 32 | 33 | const AndroidInitializationSettings initializationSettingsAndroid = 34 | AndroidInitializationSettings('@drawable/ic_stat_florae'); 35 | const InitializationSettings initializationSettings = 36 | InitializationSettings(android: initializationSettingsAndroid); 37 | flutterLocalNotificationsPlugin.initialize(initializationSettings); 38 | } 39 | 40 | Future singleNotification(String title, String body, int hashCode, 41 | {String? payload, String? sound}) async { 42 | AndroidNotificationDetails androidPlatformChannelSpecifics = 43 | AndroidNotificationDetails('care_reminder', 'Care reminder', 44 | icon: '@drawable/ic_stat_florae', 45 | channelDescription: 'Receive plants care notifications', 46 | importance: Importance.max, 47 | priority: Priority.high, 48 | ticker: title); 49 | 50 | NotificationDetails platformChannelSpecifics = 51 | NotificationDetails(android: androidPlatformChannelSpecifics); 52 | 53 | await flutterLocalNotificationsPlugin 54 | .show(hashCode, title, body, platformChannelSpecifics, payload: payload); 55 | } 56 | -------------------------------------------------------------------------------- /lib/screens/care_plant.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:florae/data/plant.dart'; 3 | import 'package:florae/screens/picture_viewer.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:intl/intl.dart'; 6 | import '../data/care.dart'; 7 | import '../data/default.dart'; 8 | import '../main.dart'; 9 | import 'manage_plant.dart'; 10 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 11 | 12 | class CarePlantScreen extends StatefulWidget { 13 | const CarePlantScreen({Key? key, required this.title}) : super(key: key); 14 | 15 | final String title; 16 | 17 | @override 18 | State createState() => _CarePlantScreen(); 19 | } 20 | 21 | class _CarePlantScreen extends State { 22 | int periodicityInHours = 1; 23 | Map careCheck = {}; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | super.dispose(); 33 | } 34 | 35 | String buildCareMessage(int daysToCare) { 36 | if (daysToCare == 0) { 37 | return AppLocalizations.of(context)!.now; 38 | } else if (daysToCare < 0) { 39 | return "${AppLocalizations.of(context)!.daysLate} ${daysToCare.abs()} ${AppLocalizations.of(context)!.days}"; 40 | } else { 41 | return "$daysToCare ${AppLocalizations.of(context)!.daysLeft}"; 42 | } 43 | } 44 | 45 | Future _showDeletePlantDialog(Plant plant) async { 46 | return showDialog( 47 | context: context, 48 | barrierDismissible: false, // user must tap button! 49 | builder: (BuildContext context) { 50 | return AlertDialog( 51 | title: Text(AppLocalizations.of(context)!.deletePlantTitle), 52 | content: SingleChildScrollView( 53 | child: ListBody( 54 | children: [ 55 | Text(AppLocalizations.of(context)!.deletePlantBody), 56 | ], 57 | ), 58 | ), 59 | actions: [ 60 | TextButton( 61 | child: Text(AppLocalizations.of(context)!.no), 62 | onPressed: () { 63 | Navigator.of(context).pop(); 64 | }, 65 | ), 66 | TextButton( 67 | child: Text(AppLocalizations.of(context)!.yes), 68 | onPressed: () async { 69 | await garden.deletePlant(plant); 70 | 71 | Navigator.popUntil(context, ModalRoute.withName('/')); 72 | }, 73 | ), 74 | ], 75 | ); 76 | }, 77 | ); 78 | } 79 | 80 | List _buildCares(BuildContext context, Plant plant) { 81 | return plant.cares.map((care) { 82 | int daysToCare = 83 | care.cycles - DateTime.now().difference(care.effected!).inDays; 84 | 85 | if (careCheck[care] == null) { 86 | careCheck[care] = daysToCare <= 0; 87 | } 88 | return CheckboxListTile( 89 | title: Text(DefaultValues.getCare(context, care.name)!.translatedName), 90 | subtitle: Text(buildCareMessage(daysToCare)), 91 | value: careCheck[care], 92 | onChanged: (bool? value) { 93 | setState(() { 94 | careCheck[care] = value; 95 | }); 96 | }, 97 | secondary: Icon(DefaultValues.getCare(context, care.name)!.icon, 98 | color: DefaultValues.getCare(context, care.name)!.color), 99 | ); 100 | }).toList(); 101 | } 102 | 103 | @override 104 | Widget build(BuildContext context) { 105 | final plant = ModalRoute.of(context)!.settings.arguments as Plant; 106 | 107 | return Scaffold( 108 | appBar: AppBar( 109 | toolbarHeight: 70, 110 | automaticallyImplyLeading: false, 111 | title: FittedBox(fit: BoxFit.fitWidth, child: Text(plant.name)), 112 | elevation: 0.0, 113 | backgroundColor: Colors.transparent, 114 | shadowColor: Colors.transparent, 115 | actions: [ 116 | IconButton( 117 | icon: const Icon(Icons.edit), 118 | iconSize: 25, 119 | color: Theme.of(context).colorScheme.primary, 120 | tooltip: AppLocalizations.of(context)!.tooltipEdit, 121 | onPressed: () async { 122 | await Navigator.push( 123 | context, 124 | MaterialPageRoute( 125 | builder: (context) => ManagePlantScreen( 126 | title: "Manage plant", update: true, plant: plant), 127 | )); 128 | }, 129 | ) 130 | ], 131 | titleTextStyle: Theme.of(context).textTheme.displayLarge, 132 | ), 133 | //passing in the ListView.builder 134 | body: SingleChildScrollView( 135 | child: Padding( 136 | padding: const EdgeInsets.all(15.0), 137 | child: Column( 138 | children: [ 139 | Card( 140 | clipBehavior: Clip.antiAliasWithSaveLayer, 141 | shape: RoundedRectangleBorder( 142 | borderRadius: BorderRadius.circular(10.0), 143 | ), 144 | elevation: 2, 145 | child: InkWell( 146 | onTap: () { 147 | if (!plant.picture!.contains("assets/")) { 148 | Navigator.push( 149 | context, 150 | MaterialPageRoute( 151 | builder: (context) => PictureViewer( 152 | picture: plant.picture, 153 | )), 154 | ); 155 | } 156 | }, 157 | child: SizedBox( 158 | child: Column( 159 | children: [ 160 | ClipRRect( 161 | child: SizedBox( 162 | height: 220, 163 | child: plant.picture!.contains("assets/") 164 | ? Image.asset( 165 | plant.picture!, 166 | fit: BoxFit.fitHeight, 167 | ) 168 | : Image.file( 169 | File(plant.picture!), 170 | fit: BoxFit.fitWidth, 171 | ), 172 | ), 173 | ), 174 | ], 175 | )), 176 | ), 177 | ), 178 | Card( 179 | semanticContainer: true, 180 | clipBehavior: Clip.antiAliasWithSaveLayer, 181 | elevation: 2, 182 | shape: RoundedRectangleBorder( 183 | borderRadius: BorderRadius.circular(10.0), 184 | ), 185 | child: Column(children: [ 186 | ListTile( 187 | leading: const Icon(Icons.topic), 188 | title: Text( 189 | AppLocalizations.of(context)!.labelDescription), 190 | subtitle: Text(plant.description)), 191 | ListTile( 192 | leading: const Icon(Icons.location_on), 193 | title: 194 | Text(AppLocalizations.of(context)!.labelLocation), 195 | subtitle: Text(plant.location ?? "")), 196 | ListTile( 197 | leading: const Icon(Icons.cake), 198 | title: 199 | Text(AppLocalizations.of(context)!.labelDayPlanted), 200 | subtitle: Text(DateFormat.yMMMMd( 201 | Localizations.localeOf(context).languageCode) 202 | .format(plant.createdAt))), 203 | ]), 204 | ), 205 | Card( 206 | semanticContainer: true, 207 | clipBehavior: Clip.antiAliasWithSaveLayer, 208 | elevation: 2, 209 | shape: RoundedRectangleBorder( 210 | borderRadius: BorderRadius.circular(10.0), 211 | ), 212 | child: Column(children: _buildCares(context, plant))), 213 | const SizedBox(height: 70), 214 | ], 215 | ), 216 | ), 217 | ), 218 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 219 | floatingActionButton: Padding( 220 | padding: const EdgeInsets.all(8.0), 221 | child: Row( 222 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 223 | children: [ 224 | FloatingActionButton.extended( 225 | heroTag: "delete", 226 | onPressed: () async { 227 | await _showDeletePlantDialog(plant); 228 | }, 229 | label: Text(AppLocalizations.of(context)!.deleteButton), 230 | icon: const Icon(Icons.delete), 231 | backgroundColor: Colors.redAccent, 232 | ), 233 | FloatingActionButton.extended( 234 | heroTag: "care", 235 | onPressed: () async { 236 | if (!careCheck.containsValue(true)) { 237 | print("NO CARES"); 238 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 239 | content: 240 | Text(AppLocalizations.of(context)!.noCaresError))); 241 | } else { 242 | careCheck.forEach((key, value) { 243 | if (value == true) { 244 | var careIndex = plant.cares 245 | .indexWhere((element) => element.name == key.name); 246 | if (careIndex != -1) { 247 | plant.cares[careIndex].effected = DateTime.now(); 248 | } 249 | } 250 | }); 251 | 252 | await garden.updatePlant(plant); 253 | Navigator.of(context).pop(); 254 | } 255 | }, 256 | label: Text(AppLocalizations.of(context)!.careButton), 257 | icon: const Icon(Icons.check), 258 | backgroundColor: Theme.of(context).colorScheme.secondary, 259 | ) 260 | ], 261 | ), 262 | )); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /lib/screens/error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | class ErrorPage extends StatelessWidget { 5 | final FlutterErrorDetails errorDetails; 6 | 7 | const ErrorPage({ 8 | Key? key, 9 | required this.errorDetails, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | resizeToAvoidBottomInset: false, 16 | appBar: AppBar( 17 | toolbarHeight: 70, 18 | automaticallyImplyLeading: false, 19 | backgroundColor: Colors.transparent, 20 | elevation: 0.0, 21 | 22 | // Here we take the value from the MyHomePage object that was created by 23 | // the App.build method, and use it to set our appbar title. 24 | title: const FittedBox(fit: BoxFit.fitWidth, child: Text("Error")), 25 | titleTextStyle: Theme.of(context).textTheme.displayLarge, 26 | ), 27 | body: Padding( 28 | padding: const EdgeInsets.symmetric(), 29 | child: Center( 30 | child: Column( 31 | mainAxisAlignment: MainAxisAlignment.center, 32 | crossAxisAlignment: CrossAxisAlignment.center, 33 | children: [ 34 | const Padding( 35 | padding: EdgeInsets.only(left: 5, bottom: 20, right: 5, top: 0), 36 | // ★~(◠︿◕✿) 37 | // (◕︿◕✿) 38 | // (◕﹏◕✿) 39 | child: Text('(◕﹏◕✿)', 40 | style: TextStyle(color: Colors.redAccent, fontSize: 45)), 41 | ), 42 | const Text( 43 | "An error has occurred.", 44 | style: TextStyle( 45 | color: Colors.redAccent, 46 | fontSize: 25, 47 | fontFamily: "NotoSans"), 48 | ), 49 | Padding( 50 | padding: const EdgeInsets.only( 51 | left: 20, bottom: 50, right: 20, top: 10), 52 | child: SelectableText(errorDetails.exception.toString(), 53 | textAlign: TextAlign.center, 54 | style: const TextStyle(fontSize: 20)), 55 | ), 56 | OutlinedButton.icon( 57 | icon: const Icon(Icons.error, size: 18), 58 | style: OutlinedButton.styleFrom( 59 | side:const BorderSide( 60 | color: Colors.transparent, 61 | ), 62 | foregroundColor: Colors.redAccent, 63 | backgroundColor: Colors.white), 64 | onPressed: () => showDialog( 65 | context: context, 66 | builder: (BuildContext context) => AlertDialog( 67 | title: const Text('Error details'), 68 | content: SingleChildScrollView( 69 | scrollDirection: Axis.vertical, //.horizontal 70 | child: SelectableText(errorDetails.toString(), 71 | textAlign: TextAlign.center, 72 | style: const TextStyle( 73 | fontSize: 12, 74 | fontFamily: "monospace")), 75 | ), 76 | actions: [ 77 | TextButton( 78 | onPressed: () => Clipboard.setData( 79 | ClipboardData(text: errorDetails.toString())), 80 | child: const Text('Copy', 81 | style: TextStyle(color: Colors.redAccent)), 82 | ), 83 | TextButton( 84 | onPressed: () => Navigator.pop(context, 'OK'), 85 | child: const Text('OK', 86 | style: TextStyle(color: Colors.redAccent)), 87 | ), 88 | ], 89 | ), 90 | ), 91 | label: const Text('Error details'), 92 | ), 93 | ], 94 | )), 95 | ), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/screens/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:background_fetch/background_fetch.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_svg/svg.dart'; 6 | import 'package:flutter_svg/flutter_svg.dart'; 7 | import 'package:florae/data/plant.dart'; 8 | import 'package:florae/notifications.dart' as notify; 9 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 10 | 11 | import 'package:intl/date_symbol_data_local.dart'; 12 | import 'package:shared_preferences/shared_preferences.dart'; 13 | 14 | import 'package:intl/intl.dart'; 15 | import 'package:responsive_grid_list/responsive_grid_list.dart'; 16 | import '../data/care.dart'; 17 | import '../data/default.dart'; 18 | import '../main.dart'; 19 | import 'manage_plant.dart'; 20 | import 'care_plant.dart'; 21 | import 'settings.dart'; 22 | 23 | enum Page { today, garden } 24 | 25 | class MyHomePage extends StatefulWidget { 26 | const MyHomePage({Key? key, required this.title}) : super(key: key); 27 | 28 | // This widget is the home page of your application. It is stateful, meaning 29 | // that it has a State object (defined below) that contains fields that affect 30 | // how it looks. 31 | 32 | // This class is the configuration for the state. It holds the values (in this 33 | // case the title) provided by the parent (in this case the App widget) and 34 | // used by the build method of the State. Fields in a Widget subclass are 35 | // always marked "final". 36 | 37 | final String title; 38 | 39 | @override 40 | State createState() => _MyHomePageState(); 41 | } 42 | 43 | class _MyHomePageState extends State { 44 | List _plants = []; 45 | Map> _cares = {}; 46 | bool _dateFilterEnabled = false; 47 | DateTime _dateFilter = DateTime.now(); 48 | Page _currentPage = Page.today; 49 | 50 | @override 51 | void dispose() { 52 | super.dispose(); 53 | } 54 | 55 | @override 56 | void initState() { 57 | super.initState(); 58 | _loadPlants(); 59 | 60 | initializeDateFormatting(); 61 | 62 | initPlatformState(); 63 | } 64 | 65 | // Platform messages are asynchronous, so we initialize in an async method. 66 | Future initPlatformState() async { 67 | // Configure BackgroundFetch. 68 | final prefs = await SharedPreferences.getInstance(); 69 | final int? notificationTempo = prefs.getInt('notificationTempo'); 70 | 71 | notify.initNotifications(AppLocalizations.of(context)!.careNotificationName, 72 | AppLocalizations.of(context)!.careNotificationDescription); 73 | 74 | try { 75 | var status = await BackgroundFetch.configure( 76 | BackgroundFetchConfig( 77 | minimumFetchInterval: notificationTempo ?? 60, 78 | forceAlarmManager: false, 79 | stopOnTerminate: false, 80 | startOnBoot: true, 81 | enableHeadless: true, 82 | requiresBatteryNotLow: false, 83 | requiresCharging: false, 84 | requiresStorageNotLow: false, 85 | requiresDeviceIdle: false, 86 | requiredNetworkType: NetworkType.NONE), 87 | _onBackgroundFetch, 88 | _onBackgroundFetchTimeout); 89 | print('[BackgroundFetch] configure success: $status'); 90 | } on Exception catch (e) { 91 | print("[BackgroundFetch] configure ERROR: $e"); 92 | } 93 | 94 | // If the widget was removed from the tree while the asynchronous platform 95 | // message was in flight, we want to discard the reply rather than calling 96 | // setState to update our non-existent appearance. 97 | if (!mounted) return; 98 | } 99 | 100 | void _onBackgroundFetch(String taskId) async { 101 | // This is the fetch-event callback. 102 | print("[BackgroundFetch] Event received: $taskId"); 103 | 104 | if (taskId == "flutter_background_fetch") { 105 | List allPlants = await garden.getAllPlants(); 106 | 107 | List plants = []; 108 | 109 | for (Plant p in allPlants) { 110 | for (Care c in p.cares) { 111 | var daysSinceLastCare = DateTime.now().difference(c.effected!).inDays; 112 | if (daysSinceLastCare != 0 && daysSinceLastCare % c.cycles == 0) { 113 | plants.add(p.name); 114 | } 115 | break; 116 | } 117 | } 118 | 119 | print("foreground florae detected plants " + plants.join(' ')); 120 | 121 | if (plants.isNotEmpty) { 122 | notify.singleNotification( 123 | AppLocalizations.of(context)!.careNotificationTitle, 124 | plants.join(' '), 125 | 7); 126 | } 127 | } 128 | BackgroundFetch.finish(taskId); 129 | } 130 | 131 | /// This event fires shortly before your task is about to timeout. You must finish any outstanding work and call BackgroundFetch.finish(taskId). 132 | void _onBackgroundFetchTimeout(String taskId) { 133 | print("[BackgroundFetch] TIMEOUT: $taskId"); 134 | BackgroundFetch.finish(taskId); 135 | } 136 | 137 | Future _showWaterAllPlantsDialog() async { 138 | return showDialog( 139 | context: context, 140 | barrierDismissible: false, // user must tap button! 141 | builder: (BuildContext context) { 142 | return AlertDialog( 143 | title: Text(AppLocalizations.of(context)!.careAll), 144 | content: SingleChildScrollView( 145 | child: ListBody( 146 | children: [ 147 | Text(AppLocalizations.of(context)!.careAllBody), 148 | ], 149 | ), 150 | ), 151 | actions: [ 152 | TextButton( 153 | child: Text(AppLocalizations.of(context)!.no), 154 | onPressed: () { 155 | Navigator.of(context).pop(); 156 | }, 157 | ), 158 | TextButton( 159 | child: Text(AppLocalizations.of(context)!.yes), 160 | onPressed: () async { 161 | await _careAllPlants(); 162 | Navigator.of(context).pop(); 163 | }, 164 | ), 165 | ], 166 | ); 167 | }, 168 | ); 169 | } 170 | 171 | Widget noPlants() { 172 | return Center( 173 | child: Padding( 174 | padding: const EdgeInsets.all(15.0), 175 | child: Column( 176 | mainAxisSize: MainAxisSize.min, 177 | children: [ 178 | SvgPicture.asset( 179 | _currentPage == Page.today 180 | ? (Theme.of(context).brightness == Brightness.dark) 181 | ? "assets/undraw_different_love_a-3-rg.svg" 182 | : "assets/undraw_fall_thyk.svg" 183 | : (Theme.of(context).brightness == Brightness.dark) 184 | ? "assets/undraw_flowers_vx06.svg" 185 | : "assets/undraw_blooming_re_2kc4.svg", 186 | semanticsLabel: 'Fall', 187 | alignment: Alignment.center, 188 | height: 250, 189 | ), 190 | Padding( 191 | padding: const EdgeInsets.all(10), 192 | //apply padding to all four sides 193 | child: Text( 194 | _currentPage == Page.today 195 | ? AppLocalizations.of(context)!.mainNoCares 196 | : AppLocalizations.of(context)!.mainNoPlants, 197 | style: TextStyle( 198 | fontFamily: 'NotoSans', 199 | fontWeight: FontWeight.w500, 200 | fontSize: 0.065 * MediaQuery.of(context).size.width, 201 | color: Theme.of(context).colorScheme.primary, 202 | ), 203 | textAlign: TextAlign.center, 204 | ), 205 | ), 206 | ], 207 | ), 208 | ), 209 | ); 210 | } 211 | 212 | String titleSelector() { 213 | if (_dateFilterEnabled) { 214 | return DateFormat.EEEE(Localizations.localeOf(context).languageCode) 215 | .format(_dateFilter) + 216 | " " + 217 | DateFormat('d').format(_dateFilter); 218 | } else if (_currentPage == Page.garden) { 219 | return AppLocalizations.of(context)!.buttonGarden; 220 | } else { 221 | return AppLocalizations.of(context)!.buttonToday; 222 | } 223 | } 224 | 225 | @override 226 | Widget build(BuildContext context) { 227 | // This method is rerun every time setState is called, for instance as done 228 | // by the _incrementCounter method above. 229 | // 230 | // The Flutter framework has been optimized to make rerunning build methods 231 | // fast, so that you can just rebuild anything that needs updating rather 232 | // than having to individually change instances of widgets. 233 | 234 | String title = titleSelector(); 235 | 236 | return Scaffold( 237 | resizeToAvoidBottomInset: false, 238 | appBar: AppBar( 239 | toolbarHeight: 70, 240 | // Here we take the value from the MyHomePage object that was created by 241 | // the App.build method, and use it to set our appbar title. 242 | title: FittedBox(fit: BoxFit.fitWidth, child: Text(title)), 243 | titleTextStyle: Theme.of(context).textTheme.displayLarge, 244 | actions: [ 245 | _currentPage == Page.today 246 | ? IconButton( 247 | icon: const Icon(Icons.checklist_rounded), 248 | iconSize: 25, 249 | color: Theme.of(context).colorScheme.primary, 250 | tooltip: AppLocalizations.of(context)!.tooltipCareAll, 251 | onPressed: () { 252 | _showWaterAllPlantsDialog(); 253 | }, 254 | ) 255 | : const SizedBox.shrink(), 256 | _currentPage == Page.today 257 | ? IconButton( 258 | icon: const Icon(Icons.calendar_today), 259 | iconSize: 25, 260 | color: Theme.of(context).colorScheme.primary, 261 | tooltip: AppLocalizations.of(context)!.tooltipShowCalendar, 262 | onPressed: () async { 263 | DateTime? result = await showDatePicker( 264 | context: context, 265 | initialDate: 266 | DateTime.now().add(const Duration(days: 1)), 267 | firstDate: DateTime.now().add(const Duration(days: 1)), 268 | lastDate: DateTime.now().add(const Duration(days: 7))); 269 | setState(() { 270 | if (result != null) { 271 | var time = TimeOfDay.now(); 272 | _dateFilter = result.add( 273 | Duration(hours: time.hour, minutes: time.minute)); 274 | _dateFilterEnabled = true; 275 | _loadPlants(); 276 | } 277 | }); 278 | }, 279 | ) 280 | : const SizedBox.shrink(), 281 | IconButton( 282 | icon: const Icon(Icons.settings), 283 | iconSize: 25, 284 | color: Theme.of(context).colorScheme.primary, 285 | tooltip: AppLocalizations.of(context)!.tooltipSettings, 286 | onPressed: () async { 287 | await Navigator.push( 288 | context, 289 | MaterialPageRoute( 290 | builder: (context) => 291 | const SettingsScreen(title: "Settings Screen"), 292 | )); 293 | }, 294 | ), 295 | ], 296 | backgroundColor: Colors.transparent, 297 | iconTheme: const IconThemeData(color: Colors.white), 298 | elevation: 0.0, 299 | ), 300 | body: _plants.isEmpty 301 | ? noPlants() 302 | : ResponsiveGridList( 303 | // Horizontal space between grid items 304 | horizontalGridSpacing: 10, 305 | // Vertical space between grid items 306 | verticalGridSpacing: 10, 307 | // Horizontal space around the grid 308 | horizontalGridMargin: 10, 309 | // Vertical space around the grid 310 | verticalGridMargin: 10, 311 | // The minimum item width (can be smaller, if the layout constraints are smaller) 312 | minItemWidth: 150, 313 | // The minimum items to show in a single row. Takes precedence over minItemWidth 314 | minItemsPerRow: 2, 315 | // The maximum items to show in a single row. Can be useful on large screens 316 | maxItemsPerRow: 6, 317 | children: _buildPlantCards(context) // Changed code 318 | ), 319 | bottomNavigationBar: NavigationBar( 320 | onDestinationSelected: (int index) { 321 | setState(() { 322 | _dateFilterEnabled = false; 323 | _currentPage = Page.values[index]; 324 | _loadPlants(); 325 | }); 326 | }, 327 | selectedIndex: _currentPage.index, 328 | destinations: [ 329 | NavigationDestination( 330 | selectedIcon: 331 | Icon(Icons.eco, color: Theme.of(context).colorScheme.surface), 332 | icon: const Icon(Icons.eco_outlined), 333 | label: AppLocalizations.of(context)!.buttonToday, 334 | ), 335 | NavigationDestination( 336 | selectedIcon: 337 | Icon(Icons.grass, color: Theme.of(context).colorScheme.surface), 338 | icon: const Icon(Icons.grass_outlined), 339 | label: AppLocalizations.of(context)!.buttonGarden, 340 | ), 341 | ], 342 | ), 343 | 344 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 345 | floatingActionButton: FloatingActionButton( 346 | onPressed: () async { 347 | await Navigator.push( 348 | context, 349 | MaterialPageRoute( 350 | builder: (context) => const ManagePlantScreen( 351 | title: "Manage plant", update: false), 352 | )); 353 | setState(() { 354 | _currentPage = Page.garden; 355 | _loadPlants(); 356 | }); 357 | }, 358 | tooltip: AppLocalizations.of(context)!.tooltipNewPlant, 359 | child: const Icon(Icons.add), 360 | backgroundColor: Theme.of(context).colorScheme.secondary, 361 | ), // This trailing comma makes auto-formatting nicer for build methods. 362 | ); 363 | } 364 | 365 | _loadPlants() async { 366 | List plants = []; 367 | Map> cares = {}; 368 | 369 | List allPlants = await garden.getAllPlants(); 370 | DateTime dateCheck = _dateFilterEnabled ? _dateFilter : DateTime.now(); 371 | 372 | bool inserted = false; 373 | bool requiresInsert = false; 374 | 375 | if (_currentPage == Page.today) { 376 | for (Plant p in allPlants) { 377 | cares[p.name] = []; 378 | for (Care c in p.cares) { 379 | var daysSinceLastCare = dateCheck.difference(c.effected!).inDays; 380 | 381 | // If calendar day selected, add only the care that must be attended on a certain day. 382 | // Past care is assumed to have been correctly attended to in due time. 383 | if (_dateFilterEnabled) { 384 | requiresInsert = 385 | daysSinceLastCare != 0 && daysSinceLastCare % c.cycles == 0; 386 | } 387 | // Else, add all unattended care, current and past 388 | else { 389 | requiresInsert = 390 | daysSinceLastCare != 0 && daysSinceLastCare / c.cycles >= 1; 391 | } 392 | if (requiresInsert) { 393 | if (!inserted) { 394 | plants.add(p); 395 | inserted = true; 396 | } 397 | cares[p.name]!.add(c.name); 398 | } 399 | } 400 | inserted = false; 401 | } 402 | } else { 403 | plants = allPlants; 404 | // Alphabetically sort 405 | plants.sort((a, b) => a.name.compareTo(b.name)); 406 | for (Plant p in allPlants) { 407 | cares[p.name] = []; 408 | for (Care c in p.cares) { 409 | cares[p.name]!.add(c.name); 410 | } 411 | } 412 | } 413 | 414 | setState(() { 415 | _cares = cares; 416 | _plants = plants; 417 | }); 418 | } 419 | 420 | _careAllPlants() async { 421 | List allPlants = await garden.getAllPlants(); 422 | 423 | DateTime dateCheck = _dateFilterEnabled ? _dateFilter : DateTime.now(); 424 | 425 | for (Plant p in allPlants) { 426 | for (Care c in p.cares) { 427 | var daysSinceLastCare = dateCheck.difference(c.effected!).inDays; 428 | if (daysSinceLastCare != 0 && daysSinceLastCare % c.cycles >= 0) { 429 | c.effected = DateTime.now(); 430 | } 431 | } 432 | await garden.updatePlant(p); 433 | } 434 | 435 | setState(() { 436 | _dateFilterEnabled = false; 437 | _loadPlants(); 438 | }); 439 | } 440 | 441 | _openPlant(Plant plant) async { 442 | await Navigator.push( 443 | context, 444 | MaterialPageRoute( 445 | builder: (context) => CarePlantScreen(title: plant.name), 446 | // Pass the arguments as part of the RouteSettings. The 447 | // DetailScreen reads the arguments from these settings. 448 | settings: RouteSettings( 449 | arguments: plant, 450 | ), 451 | ), 452 | ); 453 | setState(() { 454 | _loadPlants(); 455 | }); 456 | } 457 | 458 | List _buildCares(BuildContext context, Plant plant) { 459 | List list = []; 460 | 461 | for (var care in _cares[plant.name]!) { 462 | list.add( 463 | Icon(DefaultValues.getCare(context, care)!.icon, 464 | color: DefaultValues.getCare(context, care)!.color), 465 | ); 466 | } 467 | 468 | return list; 469 | } 470 | 471 | List _buildPlantCards(BuildContext context) { 472 | final ThemeData theme = Theme.of(context); 473 | 474 | return _plants.map((plant) { 475 | return GestureDetector( 476 | onLongPressCancel: () async { 477 | await _openPlant(plant); 478 | }, 479 | child: Card( 480 | shape: RoundedRectangleBorder( 481 | borderRadius: BorderRadius.circular(10), 482 | ), 483 | clipBehavior: Clip.antiAlias, 484 | elevation: 5, 485 | child: Column( 486 | crossAxisAlignment: CrossAxisAlignment.start, 487 | children: [ 488 | AspectRatio( 489 | aspectRatio: 18 / 12, 490 | child: plant.picture!.contains("florae_avatar") 491 | ? Image.asset( 492 | plant.picture!, 493 | fit: BoxFit.fitHeight, 494 | ) 495 | : Image.file( 496 | File(plant.picture!), 497 | fit: BoxFit.fitWidth, 498 | ), 499 | ), 500 | Expanded( 501 | child: Padding( 502 | padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), 503 | child: Column( 504 | crossAxisAlignment: CrossAxisAlignment.start, 505 | children: [ 506 | FittedBox( 507 | fit: BoxFit.fitWidth, 508 | child: Text( 509 | plant.name, 510 | style: theme.textTheme.titleLarge, 511 | maxLines: 1, 512 | ), 513 | ), 514 | const SizedBox(height: 6.0), 515 | Text( 516 | plant.description, 517 | style: theme.textTheme.titleSmall, 518 | ), 519 | const SizedBox(height: 8.0), 520 | SizedBox( 521 | height: 20.0, 522 | child: FittedBox( 523 | alignment: Alignment.centerLeft, 524 | child: plant.cares.isNotEmpty 525 | ? Row( 526 | mainAxisSize: MainAxisSize.max, 527 | children: _buildCares(context, plant)) 528 | : null, 529 | )), 530 | ], 531 | ), 532 | ), 533 | ), 534 | ], 535 | ), 536 | )); 537 | }).toList(); 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /lib/screens/manage_plant.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:florae/data/default.dart'; 4 | import 'package:florae/data/plant.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:image_picker/image_picker.dart'; 8 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 9 | 10 | import 'package:path/path.dart' as p; 11 | import 'package:path_provider/path_provider.dart'; 12 | import 'package:intl/intl.dart'; 13 | 14 | import '../data/care.dart'; 15 | import '../main.dart'; 16 | import '../utils/random.dart'; 17 | 18 | class ManagePlantScreen extends StatefulWidget { 19 | const ManagePlantScreen( 20 | {Key? key, required this.title, required this.update, this.plant}) 21 | : super(key: key); 22 | 23 | final String title; 24 | final bool update; 25 | final Plant? plant; 26 | 27 | @override 28 | State createState() => _ManagePlantScreen(); 29 | } 30 | 31 | class _ManagePlantScreen extends State { 32 | Map cares = {}; 33 | 34 | DateTime _planted = DateTime.now(); 35 | 36 | List _plants = []; 37 | 38 | final nameController = TextEditingController(); 39 | final descriptionController = TextEditingController(); 40 | final locationController = TextEditingController(); 41 | 42 | final _formKey = GlobalKey(); 43 | 44 | final ImagePicker _picker = ImagePicker(); 45 | 46 | XFile? _image; 47 | int _prefNumber = 1; 48 | 49 | Future getImageFromCam() async { 50 | var image = 51 | await _picker.pickImage(source: ImageSource.camera, imageQuality: 25); 52 | setState(() { 53 | _image = image; 54 | }); 55 | } 56 | 57 | Future getImageFromGallery() async { 58 | var image = 59 | await _picker.pickImage(source: ImageSource.gallery, imageQuality: 25); 60 | setState(() { 61 | _image = image; 62 | }); 63 | } 64 | 65 | void getPrefabImage() { 66 | if (_prefNumber < 8) { 67 | setState(() { 68 | _image = null; 69 | _prefNumber++; 70 | }); 71 | } else { 72 | setState(() { 73 | _image = null; 74 | _prefNumber = 1; 75 | }); 76 | } 77 | } 78 | 79 | void _showIntegerDialog(String care) async { 80 | FocusManager.instance.primaryFocus?.unfocus(); 81 | String tempDaysValue = ""; 82 | 83 | await showDialog( 84 | context: context, 85 | builder: (BuildContext context) { 86 | return AlertDialog( 87 | title: Text(AppLocalizations.of(context)!.selectDays), 88 | content: ListTile( 89 | leading: const Icon(Icons.loop), 90 | title: TextFormField( 91 | onChanged: (String txt) => tempDaysValue = txt, 92 | autofocus: true, 93 | initialValue: cares[care]!.cycles.toString(), 94 | keyboardType: TextInputType.number, 95 | inputFormatters: [ 96 | FilteringTextInputFormatter.digitsOnly 97 | ], 98 | ), 99 | trailing: Text(AppLocalizations.of(context)!.days)), 100 | actions: [ 101 | TextButton( 102 | child: Text(AppLocalizations.of(context)!.ok), 103 | onPressed: () { 104 | setState(() { 105 | var parsedDays = int.tryParse(tempDaysValue); 106 | if (parsedDays == null) { 107 | cares[care]!.cycles = 0; 108 | } else { 109 | cares[care]!.cycles = parsedDays; 110 | } 111 | }); 112 | Navigator.of(context).pop(); 113 | }, 114 | ) 115 | ], 116 | ); 117 | }); 118 | } 119 | 120 | @override 121 | void initState() { 122 | super.initState(); 123 | _loadPlants(); 124 | 125 | // If is an update, restore old cares 126 | if (widget.update && widget.plant != null) { 127 | for (var care in widget.plant!.cares) { 128 | cares[care.name] = Care( 129 | name: care.name, 130 | cycles: care.cycles, 131 | effected: care.effected, 132 | id: care.name.hashCode); 133 | } 134 | _planted = widget.plant!.createdAt; 135 | nameController.text = widget.plant!.name; 136 | descriptionController.text = widget.plant!.description; 137 | locationController.text = widget.plant!.location ?? ""; 138 | 139 | if (widget.plant!.picture!.contains("florae_avatar")) { 140 | String? asset = 141 | widget.plant!.picture!.replaceAll(RegExp(r'\D'), ''); // '23' 142 | _prefNumber = int.tryParse(asset) ?? 1; 143 | } else { 144 | _image = XFile(widget.plant!.picture!); 145 | } 146 | } 147 | } 148 | 149 | @override 150 | void didChangeDependencies() { 151 | super.didChangeDependencies(); 152 | 153 | // Filling in the empty cares 154 | DefaultValues.getCares(context).forEach((key, value) { 155 | if (cares[key] == null) { 156 | cares[key] = Care( 157 | cycles: value.defaultCycles, 158 | effected: DateTime.now(), 159 | name: key, 160 | id: key.hashCode); 161 | } 162 | }); 163 | } 164 | 165 | @override 166 | void dispose() { 167 | super.dispose(); 168 | } 169 | 170 | List _buildCares(BuildContext context) { 171 | List list = []; 172 | 173 | DefaultValues.getCares(context).forEach((key, value) { 174 | list.add(ListTile( 175 | trailing: const Icon(Icons.arrow_right), 176 | leading: Icon(value.icon, color: value.color), 177 | title: Text( 178 | '${value.translatedName} ${AppLocalizations.of(context)!.every}'), 179 | subtitle: cares[key]!.cycles != 0 180 | ? Text(cares[key]!.cycles.toString() + 181 | " ${AppLocalizations.of(context)!.days}") 182 | : Text(AppLocalizations.of(context)!.never), 183 | onTap: () { 184 | _showIntegerDialog(key); 185 | })); 186 | }); 187 | 188 | return list; 189 | } 190 | 191 | @override 192 | Widget build(BuildContext context) { 193 | return Scaffold( 194 | appBar: AppBar( 195 | toolbarHeight: 70, 196 | automaticallyImplyLeading: false, 197 | title: FittedBox( 198 | fit: BoxFit.fitWidth, 199 | child: widget.update 200 | ? Text(AppLocalizations.of(context)!.titleEditPlant) 201 | : Text(AppLocalizations.of(context)!.titleNewPlant)), 202 | elevation: 0.0, 203 | backgroundColor: Colors.transparent, 204 | shadowColor: Colors.transparent, 205 | titleTextStyle: Theme.of(context).textTheme.displayLarge, 206 | ), 207 | //passing in the ListView.builder 208 | body: SingleChildScrollView( 209 | child: Padding( 210 | padding: const EdgeInsets.all(15.0), 211 | child: Column( 212 | children: [ 213 | Card( 214 | clipBehavior: Clip.antiAliasWithSaveLayer, 215 | shape: RoundedRectangleBorder( 216 | borderRadius: BorderRadius.circular(10.0), 217 | ), 218 | elevation: 2, 219 | child: SizedBox( 220 | child: Column( 221 | children: [ 222 | const SizedBox(height: 10), 223 | ClipRRect( 224 | borderRadius: BorderRadius.circular(20.0), //or 15.0 225 | child: SizedBox( 226 | height: 200, 227 | child: _image == null 228 | ? Image.asset( 229 | "assets/florae_avatar_$_prefNumber.png", 230 | fit: BoxFit.fitWidth, 231 | ) 232 | : Image.file(File(_image!.path)), 233 | ), 234 | ), 235 | const SizedBox(height: 10), 236 | Row( 237 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 238 | children: [ 239 | IconButton( 240 | onPressed: getImageFromCam, 241 | icon: const Icon(Icons.add_a_photo), 242 | tooltip: 243 | AppLocalizations.of(context)!.tooltipCameraImage, 244 | ), 245 | IconButton( 246 | onPressed: getPrefabImage, 247 | icon: const Icon(Icons.refresh), 248 | tooltip: AppLocalizations.of(context)! 249 | .tooltipNextAvatar), 250 | IconButton( 251 | onPressed: getImageFromGallery, 252 | icon: const Icon(Icons.wallpaper), 253 | tooltip: 254 | AppLocalizations.of(context)!.tooltipGalleryImage, 255 | ) 256 | ], 257 | ), 258 | const SizedBox(height: 10), 259 | ], 260 | )), 261 | ), 262 | Card( 263 | semanticContainer: true, 264 | clipBehavior: Clip.antiAliasWithSaveLayer, 265 | shape: RoundedRectangleBorder( 266 | borderRadius: BorderRadius.circular(10.0), 267 | ), 268 | elevation: 2, 269 | child: Padding( 270 | padding: const EdgeInsets.all(10.0), 271 | child: Form( 272 | key: _formKey, 273 | child: Column(children: [ 274 | TextFormField( 275 | controller: nameController, 276 | validator: (name) { 277 | if (name == null || name.isEmpty) { 278 | return AppLocalizations.of(context)!.emptyError; 279 | } 280 | if (_plantExist(name)) { 281 | return AppLocalizations.of(context)!.conflictError; 282 | } 283 | return null; 284 | }, 285 | cursorColor: Theme.of(context).colorScheme.secondary, 286 | maxLength: 20, 287 | decoration: InputDecoration( 288 | icon: const Icon(Icons.local_florist), 289 | labelText: AppLocalizations.of(context)!.labelName, 290 | helperText: AppLocalizations.of(context)!.exampleName, 291 | ), 292 | ), 293 | TextFormField( 294 | keyboardType: TextInputType.multiline, 295 | minLines: 1, 296 | //Normal textInputField will be displayed 297 | maxLines: 3, 298 | // when user presses enter it will adapt to it 299 | controller: descriptionController, 300 | cursorColor: Theme.of(context).colorScheme.secondary, 301 | maxLength: 100, 302 | decoration: InputDecoration( 303 | icon: const Icon(Icons.topic), 304 | labelText: 305 | AppLocalizations.of(context)!.labelDescription, 306 | ), 307 | ), 308 | TextFormField( 309 | controller: locationController, 310 | cursorColor: Theme.of(context).colorScheme.secondary, 311 | maxLength: 20, 312 | decoration: InputDecoration( 313 | icon: const Icon(Icons.location_on), 314 | labelText: 315 | AppLocalizations.of(context)!.labelLocation, 316 | helperText: 317 | AppLocalizations.of(context)!.exampleLocation, 318 | ), 319 | ), 320 | ]), 321 | ), 322 | ), 323 | ), 324 | Card( 325 | semanticContainer: true, 326 | clipBehavior: Clip.antiAliasWithSaveLayer, 327 | elevation: 2, 328 | shape: RoundedRectangleBorder( 329 | borderRadius: BorderRadius.circular(10.0), 330 | ), 331 | child: Column(children: _buildCares(context)), 332 | ), 333 | Card( 334 | semanticContainer: true, 335 | clipBehavior: Clip.antiAliasWithSaveLayer, 336 | elevation: 2, 337 | shape: RoundedRectangleBorder( 338 | borderRadius: BorderRadius.circular(10.0), 339 | ), 340 | child: ListTile( 341 | trailing: const Icon(Icons.arrow_right), 342 | leading: const Icon(Icons.cake), 343 | enabled: !widget.update, 344 | title: Text(AppLocalizations.of(context)!.labelDayPlanted), 345 | subtitle: Text(DateFormat.yMMMMEEEEd( 346 | Localizations.localeOf(context).languageCode) 347 | .format(_planted)), 348 | onTap: () async { 349 | DateTime? result = await showDatePicker( 350 | context: context, 351 | initialDate: DateTime.now(), 352 | firstDate: DateTime(1901, 1, 1), 353 | lastDate: DateTime.now()); 354 | setState(() { 355 | _planted = result ?? DateTime.now(); 356 | }); 357 | }, 358 | ), 359 | ), 360 | const SizedBox(height: 70), 361 | ], 362 | ), 363 | ), 364 | ), 365 | floatingActionButton: FloatingActionButton.extended( 366 | onPressed: () async { 367 | String fileName = ""; 368 | String generatedId = generateRandomString(10); 369 | if (_formKey.currentState!.validate()) { 370 | if (_image != null) { 371 | final Directory directory = await getExternalStorageDirectory() ?? 372 | await getApplicationDocumentsDirectory(); 373 | fileName = directory.path + 374 | "/" + 375 | generatedId + 376 | p.extension(_image!.path); 377 | _image!.saveTo(fileName); 378 | } 379 | 380 | // Creates new plant object with previous id if we are editing 381 | // or generates a Id if we are creating a new plant 382 | final newPlant = Plant( 383 | id: widget.plant != null 384 | ? widget.plant!.id 385 | : generatedId.hashCode, 386 | name: nameController.text, 387 | createdAt: _planted, 388 | description: descriptionController.text, 389 | picture: _image != null 390 | ? fileName 391 | : "assets/florae_avatar_$_prefNumber.png", 392 | location: locationController.text, 393 | cares: []); 394 | 395 | // Assign cares to plant 396 | newPlant.cares.clear(); 397 | 398 | cares.forEach((key, value) { 399 | if (value.cycles != 0) { 400 | newPlant.cares.add(Care( 401 | cycles: value.cycles, 402 | effected: value.effected, 403 | name: key, 404 | id: key.hashCode)); 405 | } 406 | }); 407 | 408 | await garden.addOrUpdatePlant(newPlant); 409 | 410 | Navigator.popUntil(context, ModalRoute.withName('/')); 411 | } 412 | }, 413 | label: Text(AppLocalizations.of(context)!.saveButton), 414 | icon: const Icon(Icons.save), 415 | backgroundColor: Theme.of(context).colorScheme.secondary, 416 | ), 417 | ); 418 | } 419 | 420 | 421 | _loadPlants() async { 422 | List allPlants = await garden.getAllPlants(); 423 | setState(() => _plants = allPlants); 424 | } 425 | 426 | bool _plantExist(String name) => _plants.contains((plant) => plant.name == name); 427 | 428 | } 429 | -------------------------------------------------------------------------------- /lib/screens/picture_viewer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class PictureViewer extends StatelessWidget { 7 | const PictureViewer({super.key, this.picture}); 8 | 9 | final String? picture; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Center( 14 | child: InteractiveViewer( 15 | panEnabled: false, 16 | boundaryMargin: const EdgeInsets.all(100), 17 | minScale: 0.5, 18 | maxScale: 2, 19 | child: Image.file( 20 | File(picture!), 21 | fit: BoxFit.cover, 22 | ), 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/screens/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:florae/notifications.dart' as notify; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | class SettingsScreen extends StatefulWidget { 8 | const SettingsScreen({Key? key, required this.title}) : super(key: key); 9 | 10 | final String title; 11 | 12 | @override 13 | State createState() => _SettingsScreen(); 14 | } 15 | 16 | class _SettingsScreen extends State { 17 | int notificationTempo = 60; 18 | 19 | void _showIntegerDialog() async { 20 | FocusManager.instance.primaryFocus?.unfocus(); 21 | 22 | String tempHoursValue = ""; 23 | 24 | await showDialog( 25 | context: context, 26 | builder: (BuildContext context) { 27 | return AlertDialog( 28 | title: Text(AppLocalizations.of(context)!.selectHours), 29 | content: ListTile( 30 | leading: const Icon(Icons.loop), 31 | title: TextFormField( 32 | onChanged: (String txt) => tempHoursValue = txt, 33 | autofocus: true, 34 | initialValue: (notificationTempo / 60).round().toString(), 35 | keyboardType: TextInputType.number, 36 | inputFormatters: [ 37 | FilteringTextInputFormatter.digitsOnly 38 | ], 39 | ), 40 | trailing: Text(AppLocalizations.of(context)!.hours)), 41 | actions: [ 42 | TextButton( 43 | child: Text(AppLocalizations.of(context)!.ok), 44 | onPressed: () { 45 | setState(() { 46 | var parsedHours = int.tryParse(tempHoursValue); 47 | if (parsedHours == null) { 48 | notificationTempo = 1 * 60; 49 | } else { 50 | notificationTempo = parsedHours * 60; 51 | } 52 | }); 53 | Navigator.of(context).pop(); 54 | }, 55 | ) 56 | ], 57 | ); 58 | }); 59 | } 60 | 61 | Future getSharedPrefs() async { 62 | final prefs = await SharedPreferences.getInstance(); 63 | setState(() { 64 | notificationTempo = prefs.getInt('notificationTempo') ?? 60; 65 | }); 66 | } 67 | 68 | @override 69 | void initState() { 70 | super.initState(); 71 | getSharedPrefs(); 72 | } 73 | 74 | @override 75 | void dispose() { 76 | super.dispose(); 77 | } 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return Scaffold( 82 | appBar: AppBar( 83 | toolbarHeight: 70, 84 | automaticallyImplyLeading: false, 85 | title: FittedBox( 86 | fit: BoxFit.fitWidth, 87 | child: Text(AppLocalizations.of(context)!.tooltipSettings)), 88 | elevation: 0.0, 89 | backgroundColor: Colors.transparent, 90 | shadowColor: Colors.transparent, 91 | titleTextStyle: Theme.of(context).textTheme.displayLarge, 92 | ), 93 | //passing in the ListView.builder 94 | body: SingleChildScrollView( 95 | child: Padding( 96 | padding: const EdgeInsets.all(15.0), 97 | child: Column( 98 | children: [ 99 | Card( 100 | semanticContainer: true, 101 | clipBehavior: Clip.antiAliasWithSaveLayer, 102 | elevation: 2, 103 | shape: RoundedRectangleBorder( 104 | borderRadius: BorderRadius.circular(10.0), 105 | ), 106 | child: Column(children: [ 107 | ListTile( 108 | trailing: const Icon(Icons.arrow_right), 109 | leading: const Icon(Icons.alarm, color: Colors.blue), 110 | title: Text(AppLocalizations.of(context)!.notifyEvery), 111 | subtitle: notificationTempo != 0 112 | ? Text((notificationTempo / 60).round().toString() + 113 | " ${AppLocalizations.of(context)!.hours}") 114 | : Text(AppLocalizations.of(context)!.never), 115 | onTap: () { 116 | _showIntegerDialog(); 117 | }), 118 | ListTile( 119 | leading: const Icon(Icons.info_outline_rounded), 120 | subtitle: Transform.translate( 121 | offset: const Offset(-10, -5), 122 | child: 123 | Text(AppLocalizations.of(context)!.notificationInfo), 124 | ), 125 | ), 126 | ListTile( 127 | trailing: const Icon(Icons.arrow_right), 128 | leading: const Icon(Icons.circle_notifications, 129 | color: Colors.red), 130 | title: Text( 131 | AppLocalizations.of(context)!.testNotificationButton), 132 | onTap: () { 133 | notify.singleNotification( 134 | AppLocalizations.of(context)!.testNotificationTitle, 135 | AppLocalizations.of(context)!.testNotificationBody, 136 | 2); 137 | }), 138 | ]), 139 | ), 140 | Card( 141 | semanticContainer: true, 142 | clipBehavior: Clip.antiAliasWithSaveLayer, 143 | elevation: 2, 144 | shape: RoundedRectangleBorder( 145 | borderRadius: BorderRadius.circular(10.0), 146 | ), 147 | child: Column(children: [ 148 | ListTile( 149 | trailing: const Icon(Icons.arrow_right), 150 | leading: const Icon(Icons.text_snippet, 151 | color: Colors.lightGreen), 152 | title: Text( 153 | AppLocalizations.of(context)!.aboutFloraeButton), 154 | onTap: () { 155 | showAboutDialog( 156 | context: context, 157 | applicationName: 'Florae', 158 | applicationVersion: '3.0.0', 159 | applicationLegalese: '© Naval Alcalá', 160 | ); 161 | }), 162 | ])), 163 | const SizedBox(height: 70), 164 | ], 165 | ), 166 | ), 167 | ), 168 | floatingActionButton: FloatingActionButton.extended( 169 | onPressed: () async { 170 | final prefs = await SharedPreferences.getInstance(); 171 | await prefs.setInt('notificationTempo', notificationTempo); 172 | Navigator.pop(context); 173 | }, 174 | label: Text(AppLocalizations.of(context)!.saveButton), 175 | icon: const Icon(Icons.save), 176 | backgroundColor: Theme.of(context).colorScheme.secondary, 177 | ), 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/themes/darkTheme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ThemeData buildDarkThemeData() { 4 | return ThemeData( 5 | brightness: Brightness.dark, 6 | scaffoldBackgroundColor: Colors.black, 7 | primaryColor: Colors.tealAccent, 8 | fontFamily: "NotoSans", 9 | colorScheme: const ColorScheme.dark( 10 | surfaceTint: Colors.transparent, 11 | primary: Colors.white70, 12 | //onPrimary: Colors.white, 13 | secondary: Colors.tealAccent), 14 | textTheme: const TextTheme( 15 | displayLarge: TextStyle( 16 | color: Colors.white70, 17 | fontSize: 40, 18 | fontWeight: FontWeight.w800, 19 | fontFamily: "NotoSans"), 20 | titleLarge: TextStyle( 21 | color: Colors.white70, 22 | ), 23 | titleMedium: TextStyle( 24 | color: Colors.white70, 25 | )), 26 | listTileTheme: const ListTileThemeData( 27 | iconColor: Colors.white70, 28 | subtitleTextStyle: TextStyle(color: Colors.white70)), 29 | textButtonTheme: TextButtonThemeData( 30 | style: ButtonStyle( 31 | foregroundColor: 32 | WidgetStateProperty.resolveWith((state) => Colors.white70)), 33 | ), 34 | bottomNavigationBarTheme: const BottomNavigationBarThemeData( 35 | backgroundColor: Color(0xFF121212), 36 | selectedItemColor: Colors.tealAccent, 37 | unselectedItemColor: Colors.white30, 38 | elevation: 10, 39 | showUnselectedLabels: true, 40 | ), 41 | checkboxTheme: CheckboxThemeData( 42 | fillColor: WidgetStateColor.resolveWith((states) => 43 | states.contains(WidgetState.selected) 44 | ? Colors.tealAccent 45 | : Colors.transparent), 46 | checkColor: WidgetStateProperty.all(Colors.black), 47 | side: const BorderSide(width: 2.0, color: Colors.white30)), 48 | inputDecorationTheme: InputDecorationTheme( 49 | labelStyle: const TextStyle(color: Colors.white60), 50 | iconColor: WidgetStateColor.resolveWith((states) => 51 | states.contains(WidgetState.focused) 52 | ? Colors.tealAccent 53 | : Colors.white60), 54 | fillColor: Colors.tealAccent, 55 | focusColor: Colors.tealAccent, 56 | hoverColor: Colors.tealAccent, 57 | helperStyle: const TextStyle(color: Colors.white60), 58 | hintStyle: const TextStyle( 59 | decorationColor: Colors.tealAccent, color: Colors.tealAccent), 60 | enabledBorder: const UnderlineInputBorder( 61 | borderSide: BorderSide(color: Colors.white70)), 62 | focusedBorder: const UnderlineInputBorder( 63 | borderSide: BorderSide(color: Colors.teal)), 64 | ), 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /lib/themes/lightTheme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ThemeData buildLightThemeData() { 4 | return ThemeData( 5 | primaryColor: Colors.teal, 6 | scaffoldBackgroundColor: Colors.grey[100], 7 | fontFamily: "NotoSans", 8 | colorScheme: const ColorScheme.light( 9 | surfaceTint: Colors.transparent, 10 | primary: Colors.black54, 11 | secondary: Colors.teal), 12 | textTheme: const TextTheme( 13 | displayLarge: TextStyle( 14 | color: Colors.black54, 15 | fontSize: 40, 16 | fontWeight: FontWeight.w800, 17 | fontFamily: "NotoSans"), 18 | titleLarge: TextStyle( 19 | color: Colors.black, 20 | ), 21 | titleMedium: TextStyle( 22 | color: Colors.black54, 23 | )), 24 | listTileTheme: const ListTileThemeData( 25 | iconColor: Colors.black54, 26 | subtitleTextStyle: TextStyle(color: Colors.black54)), 27 | textButtonTheme: TextButtonThemeData( 28 | style: ButtonStyle( 29 | foregroundColor: 30 | WidgetStateProperty.resolveWith((state) => Colors.black)), 31 | ), 32 | bottomNavigationBarTheme: const BottomNavigationBarThemeData( 33 | backgroundColor: Colors.white, 34 | selectedItemColor: Colors.teal, 35 | elevation: 10, 36 | showUnselectedLabels: true, 37 | ), 38 | checkboxTheme: CheckboxThemeData( 39 | fillColor: WidgetStateProperty.resolveWith((states) => 40 | states.contains(WidgetState.selected) 41 | ? Colors.teal 42 | : Colors.transparent), 43 | checkColor: WidgetStateProperty.all(Colors.white), 44 | side: const BorderSide(width: 2.0, color: Colors.black54)), 45 | inputDecorationTheme: InputDecorationTheme( 46 | labelStyle: const TextStyle(color: Colors.black), 47 | iconColor: WidgetStateColor.resolveWith((states) => 48 | states.contains(WidgetState.focused) ? Colors.teal : Colors.black54), 49 | fillColor: Colors.teal, 50 | focusColor: Colors.teal, 51 | hoverColor: Colors.teal, 52 | hintStyle: 53 | const TextStyle(decorationColor: Colors.teal, color: Colors.teal), 54 | helperStyle: const TextStyle(color: Colors.black54), 55 | enabledBorder: const UnderlineInputBorder(), 56 | focusedBorder: const UnderlineInputBorder( 57 | borderSide: BorderSide(color: Colors.teal)), 58 | ), 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /lib/utils/random.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | String generateRandomString(int length) { 4 | const _chars = 5 | 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; 6 | Random _rnd = Random(); 7 | 8 | return String.fromCharCodes(Iterable.generate( 9 | length, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)))); 10 | } -------------------------------------------------------------------------------- /metadata/en-US/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Initial app version. -------------------------------------------------------------------------------- /metadata/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | * New Material 3 themes added 2 | * Now you can see in full screen the image of the plant. 3 | * Now it is possible to change the plant name. 4 | * Adapted plant grid for large screens 5 | * Fixed a problem with displaying the age of the plant 6 | * New languages added -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Florae is an open source application that will allow you to keep track of the care of your plants while respecting your freedom and privacy. 2 | 3 | Set reminders for basic plant care such as watering, pruning, cleaning, fertilizing, spraying, rotating, and transplanting. 4 | 5 | Consult future plant cares for better planning of your time, adjust the frequency of care alerts and don't forget to water your plants ever again. -------------------------------------------------------------------------------- /metadata/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/metadata/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeri/Florae/0dad44caeb679a5015442cd2771a832f81e7c753/metadata/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Keep care of your green, leafy best friends. -------------------------------------------------------------------------------- /metadata/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Florae -------------------------------------------------------------------------------- /metadata/es/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Versión inicial. -------------------------------------------------------------------------------- /metadata/es/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | * Se añaden nuevos temas Material 3. 2 | * Ahora puedes ver en pantalla completa la foto de tu planta. 3 | * Ahora es posible cambiar el nombre de la planta. 4 | * Se mejora la disposición de elementos para pantallas grandes. 5 | * Se corrige un problema al mostrar la edad de la planta. 6 | * Se añaden nuevos idiomas. -------------------------------------------------------------------------------- /metadata/es/full_description.txt: -------------------------------------------------------------------------------- 1 | Florae es una aplicación de código abierto que te permitirá llevar un control del cuidado de tus plantas respetando tu libertad y privacidad. 2 | 3 | Establece recordatorios para los cuidados básicos de tus plantas como riego, poda, limpieza, abonado, pulverización, rotación y trasplante. 4 | 5 | Consulta futuros cuidados de tus plantas para planificar mejor tu tiempo, ajusta la frecuencia de las alertas y no te olvides de regarlas nunca más. -------------------------------------------------------------------------------- /metadata/es/short_description.txt: -------------------------------------------------------------------------------- 1 | Cuida a tus verdes y frondosas amigas. -------------------------------------------------------------------------------- /metadata/es/title.txt: -------------------------------------------------------------------------------- 1 | Florae -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: florae 2 | description: Keep care of your green, leafy best friends 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 3.0.0+5 19 | 20 | environment: 21 | sdk: '>=3.3.4 <4.0.0' 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | path_provider: ^2.1.3 34 | image_picker: ^1.1.2 35 | flutter_svg: ^2.0.10+1 36 | json_annotation: ^4.9.0 37 | 38 | flutter_localizations: 39 | sdk: flutter 40 | flutter_local_notifications: ^17.1.2 41 | shared_preferences: ^2.0.17 42 | responsive_grid_list: ^1.3.0 43 | 44 | ## Static version for F-Droid deploy 45 | background_fetch: 1.3.5 46 | 47 | dev_dependencies: 48 | build_runner: ^2.4.11 49 | json_serializable: ^6.8.0 50 | flutter_test: 51 | sdk: flutter 52 | 53 | # The "flutter_lints" package below contains a set of recommended lints to 54 | # encourage good coding practices. The lint set provided by the package is 55 | # activated in the `analysis_options.yaml` file located at the root of your 56 | # package. See that file for information about deactivating specific lint 57 | # rules and activating additional ones. 58 | flutter_lints: ^1.0.0 59 | 60 | # For information on the generic Dart part of this file, see the 61 | # following page: https://dart.dev/tools/pub/pubspec 62 | 63 | # The following section is specific to Flutter. 64 | flutter: 65 | generate: true 66 | # The following line ensures that the Material Icons font is 67 | # included with your application, so that you can use the icons in 68 | # the material Icons class. 69 | uses-material-design: true 70 | 71 | # To add assets to your application, add an assets section, like this: 72 | assets: 73 | - assets/card-sample-image.jpg 74 | - assets/undraw_fall_thyk.svg 75 | - assets/undraw_gardening_re_e658.svg 76 | - assets/undraw_flowers_vx06.svg 77 | - assets/undraw_blooming_re_2kc4.svg 78 | - assets/undraw_different_love_a-3-rg.svg 79 | - assets/florae_avatar_1.png 80 | - assets/florae_avatar_2.png 81 | - assets/florae_avatar_3.png 82 | - assets/florae_avatar_4.png 83 | - assets/florae_avatar_5.png 84 | - assets/florae_avatar_6.png 85 | - assets/florae_avatar_7.png 86 | - assets/florae_avatar_8.png 87 | 88 | # An image asset can refer to one or more resolution-specific "variants", see 89 | # https://flutter.dev/assets-and-images/#resolution-aware. 90 | 91 | # For details regarding adding assets from package dependencies, see 92 | # https://flutter.dev/assets-and-images/#from-packages 93 | 94 | # To add custom fonts to your application, add a fonts section here, 95 | # in this "flutter" section. Each entry in this list should have a 96 | # "family" key with the font family name, and a "fonts" key with a 97 | # list giving the asset and other descriptors for the font. For 98 | # example: 99 | fonts: 100 | - family: NotoSans 101 | fonts: 102 | - asset: assets/NotoSans-Regular.ttf 103 | - asset: assets/NotoSans-Italic.ttf 104 | style: italic 105 | - asset: assets/NotoSans-Bold.ttf 106 | - asset: assets/NotoSans-BoldItalic.ttf 107 | style: italic 108 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:florae/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const FloraeApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------