├── .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 |
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 |
33 |
34 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
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 |
--------------------------------------------------------------------------------