commit 2a8805f801f4181d3b405a9318b7c14a87b0c8ed Author: joerg Date: Sun Jun 25 10:13:39 2023 +0200 Base Verion diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c21169b --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# Do not remove or rename entries in this file, only add new ones +# See https://github.com/flutter/flutter/issues/128635 for more context. + +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.classpath +.project +.settings/ +.vscode/* + +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/api_docs.zip +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ +.pub-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock +!.vscode/settings.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..ca5a3bc --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,35 @@ +stages: + - test + - analyze + +code_quality: + stage: test + image: "cirrusci/flutter:1.22.5" + before_script: + - cd "$CI_PROJECT_DIR/garden_planner" + - pub global activate dart_code_metrics + - export PATH="$PATH:$HOME/.pub-cache/bin" + script: + - cd "$CI_PROJECT_DIR/garden_planner" + - metrics lib -r codeclimate > gl-code-quality-report.json + artifacts: + reports: + codequality: garden_planner/gl-code-quality-report.json + +analyze:sonar: + stage: analyze + image: + name: sonarsource/sonar-scanner-cli:4.5 + entrypoint: [""] + variables: + # Defines the location of the analysis task cache + SONAR_USER_HOME: "${CI_PROJECT_DIR}/garden_planner/.sonar" + # Shallow cloning needs to be disabled. + # See https://docs.sonarqube.org/latest/analysis/gitlab-cicd/. + GIT_DEPTH: 0 + cache: + key: "${CI_JOB_NAME}" + paths: + - garden_planner/.sonar/cache + script: + - sonar-scanner diff --git a/CPD Idee.msg b/CPD Idee.msg new file mode 100644 index 0000000..c94fc2e Binary files /dev/null and b/CPD Idee.msg differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..59a9bc3 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Beet Pflanz-App + +[![pipeline status](https://gitlab.vierling.cloud/Joerg/cpd_project/badges/main/pipeline.svg)](https://gitlab.vierling.cloud/Joerg/cpd_project/-/commits/main) + +[![pipeline status](https://gitlab.vierling.cloud/Joerg/cpd_project/badges/main/pipeline.svg)](https://gitlab.vierling.cloud/Joerg/cpd_project/-/commits/main) + + + +Die Beet Pflanz-App ist eine Flutter-Anwendung, die im Rahmen des Studiums an der HS-Mannheim für CPD entwickelt wurde. +Die App unterstützt den Benutzer sein Beet zu Planen und die optimalen Setzpositionen zu bestimmen. +Zudem können die wichtigen Informationen der Pflanzen über das Jahr angezeigt werden. + +Das Beet kann gespeichert werden und wird beim starten geladen + +## Funktionen + +- Darstellung der Beetreihen und Pflanzenplatzierungen +- Drag-and-Drop zum Hinzufügen von Pflanzen +- Entfernung von gepflanzeten Pflanzen. +- Option zur Anzeige von Platzanforderungen für jede Reihe +- Anzeige von wichtigen Informationen der Pflanzen über das ganze Jahr +- Speichern des aktuellen Beets +- Laden des gespeicherten Beets + +### Zukünftige Funktionen +- Anzeigen der Wassermengen der Einzelnen Pflanzen bzw. Rhein angezeigt werden. +- Verwalten von mehreren Beeten + +#### Installation + +1. Flutter muss installiert sein +2. Die App befindet sich im Unterordener "garden_planner" +3. Öffnen Sie die Datei lib/constants.dart und stellen Sie sicher, dass die richtige API-URL eingetragen ist. + Wenn Sie die App lokal ausführen möchten, passen Sie die API-URL auf http://localhost:3000 an. +4. Lokale Backend-Dienste können über Docker-Compose gestartet werden damit http://localhost:3000 verfügbar ist + Befehl "docker-compose up --build" \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..b9f36f7 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,11 @@ +FROM node:latest + +WORKDIR /app +COPY package*.json ./ + +RUN npm install + +COPY . . +EXPOSE 3000 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/api/init.sql b/api/init.sql new file mode 100644 index 0000000..2dc10c4 --- /dev/null +++ b/api/init.sql @@ -0,0 +1,113 @@ +-- CREATE TABLE +CREATE TABLE IF NOT EXISTS plants ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + description VARCHAR(255), + water_requirement REAL, + horizontal_space REAL, + vertical_space REAL, + image_path VARCHAR(255) +); + +CREATE TABLE IF NOT EXISTS plant_times ( + id SERIAL PRIMARY KEY, + plant_id INTEGER, + color VARCHAR(255), + description VARCHAR(255), + from_date DATE, + until_date DATE, + action_needed boolean, + FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS beets ( + id SERIAL PRIMARY KEY, + plant_id INTEGER, + position INTEGER, + beet_row INTEGER, + FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE +); + + +INSERT INTO plants (name, description, horizontal_space, vertical_space, water_requirement, image_path) +VALUES + ('Tomate', '', 0.6, 0.8, 0.8, 'lib/assets/plants/tomatoes-gc17bf34c6_640.jpg'), + ('Kopfsalat', 'Maikönig', 0.25, 0.25, 0.5, 'lib/assets/plants/salad-seedling-g46a52dd37_640.jpg'), + ('Radieschen', '', 0.15, 0.2, 0.4, 'lib/assets/plants/root-g27af04562_640.jpg'), + ('Himbeere', 'Nugana', 0.4, 0.6, 0.9, 'lib/assets/plants/raspberries-ge56ab3ffc_640.jpg'), + ('Himbeere', 'Glen Ample', 0.4, 0.6, 0.9, 'lib/assets/plants/raspberries-gce73a006c_640.jpg'), + ('Jostabeere', '', 2.5, 2.5, 1.2, 'lib/assets/plants/jostaberry-gdf8566383_640.jpg'), + ('Johannisbeere', '', 0.4, 0.6, 0.8, 'lib/assets/plants/currant-geaf055095_640.jpg'), + ('Brombeere', 'Navaho', 1, 1, 1.1, 'lib/assets/plants/blackberries-gae933f2d8_640.jpg'), + ('Karotte', '', 0.15, 0.2, 0.7, 'lib/assets/plants/carrot.jpg'), + ('Gurke', '', 0.4, 0.6, 0.6, 'lib/assets/plants/cucumber.jpg'), + ('Paprika', '', 0.3, 0.3, 0.5, 'lib/assets/plants/pepper.jpg'), + ('Erdbeere', '', 0.3, 0.3, 0.8, 'lib/assets/plants/strawberry.jpg'), + ('Basilikum', '', 0.2, 0.2, 0.3, 'lib/assets/plants/basil.jpg'); + +INSERT INTO plant_times (plant_id, from_date, until_date, description, action_needed, color) +VALUES + -- Tomate + (1, '2023-04-01', '2023-05-15', 'Aussaat', TRUE, '4294961979'), + (1, '2023-05-15', '2023-06-15', 'Wachstumsphase', FALSE, '438858537'), + (1, '2023-06-15', '2023-07-31', 'Erntezeit', FALSE, '4294198070'), + + -- Kopfsalat + (2, '2023-04-01', '2023-06-01', 'Aussaat', TRUE, '4294961979'), + (2, '2023-06-01', '2023-07-15', 'Wachstumsphase', FALSE, '438858537'), + (2, '2023-07-15', '2023-08-31', 'Erntezeit', FALSE, '4294198070'), + + -- Radieschen + (3, '2023-03-15', '2023-05-01', 'Aussaat', TRUE, '4294961979'), + (3, '2023-05-01', '2023-06-15', 'Wachstumsphase', FALSE, '438858537'), + (3, '2023-06-15', '2023-07-31', 'Erntezeit', FALSE, '4294198070'), + + -- Himbeere (Nugana) + (4, '2023-04-15', '2023-05-31', 'Aussaat', TRUE, '4294961979'), + (4, '2023-05-31', '2023-07-15', 'Wachstumsphase', FALSE, '438858537'), + (4, '2023-07-15', '2023-08-31', 'Erntezeit', FALSE, '4294198070'), + + -- Himbeere (Glen Ample) + (5, '2023-04-15', '2023-05-31', 'Aussaat', TRUE, '4294961979'), + (5, '2023-05-31', '2023-07-15', 'Wachstumsphase', FALSE, '438858537'), + (5, '2023-07-15', '2023-08-31', 'Erntezeit', FALSE, '4294198070'), + + -- Jostabeere + (6, '2023-03-01', '2023-05-01', 'Aussaat', TRUE, '4294961979'), + (6, '2023-05-01', '2023-07-01', 'Wachstumsphase', FALSE, '438858537'), + (6, '2023-07-01', '2023-09-15', 'Erntezeit', FALSE, '4294198070'), + + -- Johannisbeere + (7, '2023-03-15', '2023-05-01', 'Aussaat', TRUE, '4294961979'), + (7, '2023-05-01', '2023-07-01', 'Wachstumsphase', FALSE, '438858537'), + (7, '2023-07-01', '2023-08-31', 'Erntezeit', FALSE, '4294198070'), + + -- Brombeere (Navaho) + (8, '2023-04-15', '2023-06-01', 'Aussaat', TRUE, '4294961979'), + (8, '2023-06-01', '2023-07-31', 'Wachstumsphase', FALSE, '438858537'), + (8, '2023-07-31', '2023-09-30', 'Erntezeit', FALSE, '4294198070'), + + -- Karotte + (9, '2023-04-01', '2023-05-15', 'Aussaat', TRUE, '4294961979'), + (9, '2023-05-15', '2023-07-01', 'Wachstumsphase', FALSE, '438858537'), + (9, '2023-07-01', '2023-08-31', 'Erntezeit', FALSE, '4294198070'), + + -- Gurke + (10, '2023-04-15', '2023-06-01', 'Aussaat', TRUE, '4294961979'), + (10, '2023-06-01', '2023-08-01', 'Wachstumsphase', FALSE, '438858537'), + (10, '2023-08-01', '2023-09-30', 'Erntezeit', FALSE, '4294198070'), + + -- Paprika + (11, '2023-03-15', '2023-05-15', 'Aussaat', TRUE, '4294961979'), + (11, '2023-05-15', '2023-07-01', 'Wachstumsphase', FALSE, '438858537'), + (11, '2023-07-01', '2023-09-30', 'Erntezeit', FALSE, '4294198070'), + + -- Erdbeere + (12, '2023-03-01', '2023-05-01', 'Aussaat', TRUE, '4294961979'), + (12, '2023-05-01', '2023-07-01', 'Wachstumsphase', FALSE, '438858537'), + (12, '2023-07-01', '2023-08-31', 'Erntezeit', FALSE, '4294198070'), + + -- Basilikum + (13, '2023-03-15', '2023-04-30', 'Aussaat', TRUE, '4294961979'), + (13, '2023-04-30', '2023-06-15', 'Wachstumsphase', FALSE, '438858537'), + (13, '2023-06-15', '2023-08-15', 'Erntezeit', FALSE, '4294198070'); \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json new file mode 100644 index 0000000..628fef9 --- /dev/null +++ b/api/package-lock.json @@ -0,0 +1,811 @@ +{ + "name": "api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.18.2", + "pg": "^8.11.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pg": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.0.tgz", + "integrity": "sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.0", + "pg-pool": "^3.6.0", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz", + "integrity": "sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.0.tgz", + "integrity": "sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz", + "integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..ab4a823 --- /dev/null +++ b/api/package.json @@ -0,0 +1,18 @@ +{ + "name": "api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.18.2", + "pg": "^8.11.0" + } +} diff --git a/api/server.js b/api/server.js new file mode 100644 index 0000000..b88e567 --- /dev/null +++ b/api/server.js @@ -0,0 +1,126 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const cors = require('cors'); +const { Pool } = require('pg'); + +const app = express(); +const PORT = 3000; + +app.use(cors()); // Enable CORS +app.use(bodyParser.json()); // Parse JSON request bodies + +const pool = new Pool({ + host: 'gardenplanner-db', + port: 5432, + database: 'gardenPlaner', + user: 'garden', + password: 'garden', +}); + +app.get('/plants/:id', (req, res) => { + const plantId = req.params.id; + + pool.query('SELECT * FROM plants WHERE id = $1', [plantId]) + .then((result) => { + const plant = result.rows[0]; + if (!plant) { + res.status(404).json({ error: 'Pflanze nicht gefunden' }); + return; + } + + pool.query('SELECT * FROM plant_times WHERE plant_id = $1', [plantId]) + .then((result) => { + const plantTimes = result.rows; + + const plantWithTimes = { + ...plant, + times: plantTimes + }; + + res.json(plantWithTimes); + }) + .catch((error) => { + console.error('Fehler beim landen der Zeiten', error); + res.status(500).json({ error: 'Fehler beim landen der Zeiten' }); + }); + }) + .catch((error) => { + console.error('Fehler beim landen der Zeiten:', error); + res.status(500).json({ error: 'Fehler beim landen der Zeiten' }); + }); +}); + +app.get('/plants', (req, res) => { + Promise.all([ + pool.query('SELECT * FROM plants'), + pool.query('SELECT * FROM plant_times') + ]) + .then(([plantsResult, plantTimesResult]) => { + const rawPlants = plantsResult.rows; + const plantTimes = plantTimesResult.rows; + + const plants = rawPlants.map((plant) => ({ + ...plant, + times: plantTimes.filter((time) => time.plant_id === plant.id) + })); + + res.json(plants); + }) + .catch((error) => { + console.error('Failed to fetch plants and plant times:', error); + res.status(500).json({ error: 'Failed to fetch plants and plant times' }); + }); +}); + +app.get('/beet', (req, res) => { + + pool.query('SELECT * FROM beets') + .then(( beetResult) => { + const beet = beetResult.rows; + + res.json(beet); + }) + .catch((error) => { + console.error('Failed to fetch plants and plant times:', error); + res.status(500).json({ error: 'Failed to fetch plants and plant times' }); + }); +}); + +app.post('/beet', (req, res) => { + const beetEntries = req.body; + + console.log(beetEntries); + + if (!beetEntries || !Array.isArray(beetEntries)) { + res.status(400).json({ error: 'falscher body' }); + return; + } + + const clearQuery = 'DELETE FROM beets'; + const insertQuery = 'INSERT INTO beets (plant_id, position, beet_row) VALUES ($1, $2, $3)'; + const values = beetEntries.map(({ plantId, position, beet_row }) => [plantId, position, beet_row]); + + pool + .connect() + .then((client) => { + return client + .query(clearQuery) // Clear the beets table + .then(() => + Promise.all( + values.map((params) => client.query(insertQuery, params)) + ) + ) + .finally(() => client.release()); + }) + .then(() => { + res.json({ message: 'Beet gespeichert' }); + }) + .catch((error) => { + console.error('Fehler beim speichern:', error); + res.status(500).json({ error: 'Fehler beim speichern' }); + }); +}); + +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..9196fa2 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,25 @@ +version: '3' + +services: + gardenplanner-db: + image: postgres + restart: always + environment: + POSTGRES_USER: garden + POSTGRES_PASSWORD: garden + POSTGRES_DB: gardenPlaner + volumes: + - ./api/init.sql:/docker-entrypoint-initdb.d/init.sql + gardenplanner-api: + build: ./api + restart: always + depends_on: + - gardenplanner-db + ports: + - 3000:3000 + adminer: + image: adminer + restart: always + ports: + - 8080:8080 + diff --git a/garden_planner/.metadata b/garden_planner/.metadata new file mode 100644 index 0000000..49afbec --- /dev/null +++ b/garden_planner/.metadata @@ -0,0 +1,39 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: android + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: linux + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: web + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: windows + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/garden_planner/analysis_options.yaml b/garden_planner/analysis_options.yaml new file mode 100644 index 0000000..0bd999b --- /dev/null +++ b/garden_planner/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: diff --git a/garden_planner/android/.gitignore b/garden_planner/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/garden_planner/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/garden_planner/android/app/build.gradle b/garden_planner/android/app/build.gradle new file mode 100644 index 0000000..b54d2f4 --- /dev/null +++ b/garden_planner/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.garden_planner" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/garden_planner/android/app/src/debug/AndroidManifest.xml b/garden_planner/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..da5b0f1 --- /dev/null +++ b/garden_planner/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/garden_planner/android/app/src/main/AndroidManifest.xml b/garden_planner/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5c99cac --- /dev/null +++ b/garden_planner/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/garden_planner/android/app/src/main/kotlin/com/example/cpd_project/MainActivity.kt b/garden_planner/android/app/src/main/kotlin/com/example/cpd_project/MainActivity.kt new file mode 100644 index 0000000..ec99707 --- /dev/null +++ b/garden_planner/android/app/src/main/kotlin/com/example/cpd_project/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.cpd_project + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() { +} diff --git a/garden_planner/android/app/src/main/kotlin/com/example/garden_planner/MainActivity.kt b/garden_planner/android/app/src/main/kotlin/com/example/garden_planner/MainActivity.kt new file mode 100644 index 0000000..ed9b799 --- /dev/null +++ b/garden_planner/android/app/src/main/kotlin/com/example/garden_planner/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.garden_planner + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() { +} diff --git a/garden_planner/android/app/src/main/res/drawable-v21/launch_background.xml b/garden_planner/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..ff7cf00 --- /dev/null +++ b/garden_planner/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/garden_planner/android/app/src/main/res/drawable/launch_background.xml b/garden_planner/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..af6e046 --- /dev/null +++ b/garden_planner/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/garden_planner/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/garden_planner/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/garden_planner/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/garden_planner/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/garden_planner/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/garden_planner/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/garden_planner/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/garden_planner/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/garden_planner/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/garden_planner/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/garden_planner/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/garden_planner/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/garden_planner/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/garden_planner/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/garden_planner/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/garden_planner/android/app/src/main/res/values-night/styles.xml b/garden_planner/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..ec4417d --- /dev/null +++ b/garden_planner/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/garden_planner/android/app/src/main/res/values/styles.xml b/garden_planner/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..b851f71 --- /dev/null +++ b/garden_planner/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/garden_planner/android/app/src/profile/AndroidManifest.xml b/garden_planner/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..da5b0f1 --- /dev/null +++ b/garden_planner/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/garden_planner/android/build.gradle b/garden_planner/android/build.gradle new file mode 100644 index 0000000..713d7f6 --- /dev/null +++ b/garden_planner/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/garden_planner/android/gradle.properties b/garden_planner/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/garden_planner/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/garden_planner/android/gradle/wrapper/gradle-wrapper.properties b/garden_planner/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/garden_planner/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/garden_planner/android/settings.gradle b/garden_planner/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/garden_planner/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/garden_planner/lib/api/api_entities/beet_entry.dart b/garden_planner/lib/api/api_entities/beet_entry.dart new file mode 100644 index 0000000..5c16366 --- /dev/null +++ b/garden_planner/lib/api/api_entities/beet_entry.dart @@ -0,0 +1,13 @@ +class BeetEntry { + final int plantId; + final int position; + final int beetRow; + + BeetEntry(this.plantId, this.position, this.beetRow); + + Map toJson() => { + 'plantId': plantId, + 'position': position, + 'beet_row': beetRow, + }; +} diff --git a/garden_planner/lib/api/garden_api.service.dart b/garden_planner/lib/api/garden_api.service.dart new file mode 100644 index 0000000..91ddda8 --- /dev/null +++ b/garden_planner/lib/api/garden_api.service.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:garden_planner/api/api_entities/beet_entry.dart'; +import 'package:garden_planner/entities/beet.dart'; +import 'package:garden_planner/entities/beet_entry_return.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/entities/plant_time.dart'; + +import 'http_connection.dart'; + +class GardenApiService { + final HttpConnector httpConnector; + + GardenApiService(this.httpConnector); + + Future> getAllAvailablePlants() async { + final response = await httpConnector.getAllPlants(); + + if (response.statusCode == 200) { + final List data = json.decode(response.body); + + return data.map((plantData) => convertPlant(plantData)).toList(); + } else { + throw Exception('Failed to fetch plants'); + } + } + + Future saveBeet(Beet beet) async { + final beetEntries = _generateBeetEntries(beet); + + final body = + jsonEncode(beetEntries.map((entry) => entry.toJson()).toList()); + final beetSaved = (await httpConnector.saveBeet(body)).statusCode == 200; + + return beetSaved + ? 'Beet wurde erfolgreich gespeichert' + : 'Fehler beim Speichern des Beets.'; + } + + Future> getBeet() async { + final response = await httpConnector.getBeet(); + + if (response.statusCode == 200) { + final List sortedBeetEntriesFormDB = + _sortBeetFromDB(response.body); + + final List beetReturn = []; + + for (final entry in sortedBeetEntriesFormDB) { + final plant = await getPlant(entry['plant_id'] as int); + beetReturn.add(BeetApiEntryReturn(plant, entry['beet_row'] as int)); + } + + return beetReturn; + } else { + throw Exception('Failed to getBeet from api'); + } + } + + List _sortBeetFromDB(String response) { + final List entries = json.decode(response); + + entries.sort((a, b) { + final int rowA = a['beet_row'] as int; + final int rowB = b['beet_row'] as int; + final int positionA = a['position'] as int; + final int positionB = b['position'] as int; + + if (rowA == rowB) { + return positionA.compareTo(positionB); + } else { + return rowA.compareTo(rowB); + } + }); + + return entries; + } + + Future getPlant(int id) async { + final response = await httpConnector.getPlant(id); + + if (response.statusCode == 200) { + final dynamic data = json.decode(response.body); + + return convertPlant(data); + } else { + throw Exception('Failed to fetch plant with id: $id'); + } + } + + Plant convertPlant(dynamic plantData) { + final List timesData = plantData['times']; + + final plantTimes = timesData + .map((time) => PlantTime( + color: Color(int.parse(time['color'])), + description: time['description'] as String, + from: DateTime.parse(time['from_date']), + until: DateTime.parse(time['until_date']), + action: time['action_needed'] as bool, + )) + .toList(); + + final plant = Plant( + id: plantData['id'] as int, + name: plantData['name'] as String, + waterRequirement: plantData['water_requirement'].toDouble(), + horizontalSpace: plantData['horizontal_space'].toDouble(), + verticalSpace: plantData['vertical_space'].toDouble(), + supType: plantData['description'] as String, + imagePath: plantData['image_path'].toString(), + times: plantTimes, + ); + + return plant; + } + + List _generateBeetEntries(Beet beet) { + final entries = []; + + for (int row = 0; row < beet.beetRows.length; row++) { + for (int position = 0; + position < beet.beetRows[row].plants.length; + position++) { + final plantId = beet.beetRows[row].plants[position].id; + final entry = BeetEntry(plantId, position, row); + entries.add(entry); + } + } + + return entries; + } +} diff --git a/garden_planner/lib/api/http_connection.dart b/garden_planner/lib/api/http_connection.dart new file mode 100644 index 0000000..640f1be --- /dev/null +++ b/garden_planner/lib/api/http_connection.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import '../constance.dart'; + +//Not testet becase only wrap around http +class HttpConnector { + static final String _apiLocation = Constance.apiLocation; + + Future getAllPlants() async { + final url = Uri.parse('$_apiLocation/plants'); + final response = await http.get(url); + + return response; + } + + Future getPlant(int id) async { + final url = Uri.parse('$_apiLocation/plants/$id'); + final response = await http.get(url); + + return response; + } + + Future getBeet() async { + final url = Uri.parse('$_apiLocation/beet'); + final response = await http.get(url); + + return response; + } + + Future saveBeet(String body) async { + final url = Uri.parse('$_apiLocation/beet'); + final headers = {'Content-Type': 'application/json'}; + + final response = await http.post( + url, + headers: headers, + body: utf8.encode(body), + ); + + return response; + } +} diff --git a/garden_planner/lib/assets/layout/height-64.png b/garden_planner/lib/assets/layout/height-64.png new file mode 100644 index 0000000..384bdda Binary files /dev/null and b/garden_planner/lib/assets/layout/height-64.png differ diff --git a/garden_planner/lib/assets/layout/planting-64-white.png b/garden_planner/lib/assets/layout/planting-64-white.png new file mode 100644 index 0000000..dbd26fc Binary files /dev/null and b/garden_planner/lib/assets/layout/planting-64-white.png differ diff --git a/garden_planner/lib/assets/layout/planting-64.png b/garden_planner/lib/assets/layout/planting-64.png new file mode 100644 index 0000000..3e5ec9a Binary files /dev/null and b/garden_planner/lib/assets/layout/planting-64.png differ diff --git a/garden_planner/lib/assets/layout/width-64.png b/garden_planner/lib/assets/layout/width-64.png new file mode 100644 index 0000000..f694269 Binary files /dev/null and b/garden_planner/lib/assets/layout/width-64.png differ diff --git a/garden_planner/lib/assets/layout/width_square.png b/garden_planner/lib/assets/layout/width_square.png new file mode 100644 index 0000000..ba643d9 Binary files /dev/null and b/garden_planner/lib/assets/layout/width_square.png differ diff --git a/garden_planner/lib/assets/plants/basil.jpg b/garden_planner/lib/assets/plants/basil.jpg new file mode 100644 index 0000000..01f5fc8 Binary files /dev/null and b/garden_planner/lib/assets/plants/basil.jpg differ diff --git a/garden_planner/lib/assets/plants/blackberries-gae933f2d8_640.jpg b/garden_planner/lib/assets/plants/blackberries-gae933f2d8_640.jpg new file mode 100644 index 0000000..78e0ad8 Binary files /dev/null and b/garden_planner/lib/assets/plants/blackberries-gae933f2d8_640.jpg differ diff --git a/garden_planner/lib/assets/plants/carrot.jpg b/garden_planner/lib/assets/plants/carrot.jpg new file mode 100644 index 0000000..272da97 Binary files /dev/null and b/garden_planner/lib/assets/plants/carrot.jpg differ diff --git a/garden_planner/lib/assets/plants/cucumber.jpg b/garden_planner/lib/assets/plants/cucumber.jpg new file mode 100644 index 0000000..d227b9e Binary files /dev/null and b/garden_planner/lib/assets/plants/cucumber.jpg differ diff --git a/garden_planner/lib/assets/plants/currant-geaf055095_640.jpg b/garden_planner/lib/assets/plants/currant-geaf055095_640.jpg new file mode 100644 index 0000000..5faae72 Binary files /dev/null and b/garden_planner/lib/assets/plants/currant-geaf055095_640.jpg differ diff --git a/garden_planner/lib/assets/plants/horizontal-water-pipe-64.png b/garden_planner/lib/assets/plants/horizontal-water-pipe-64.png new file mode 100644 index 0000000..53a9768 Binary files /dev/null and b/garden_planner/lib/assets/plants/horizontal-water-pipe-64.png differ diff --git a/garden_planner/lib/assets/plants/jostaberry-gdf8566383_640.jpg b/garden_planner/lib/assets/plants/jostaberry-gdf8566383_640.jpg new file mode 100644 index 0000000..ceb6519 Binary files /dev/null and b/garden_planner/lib/assets/plants/jostaberry-gdf8566383_640.jpg differ diff --git a/garden_planner/lib/assets/plants/pepper.jpg b/garden_planner/lib/assets/plants/pepper.jpg new file mode 100644 index 0000000..91bc932 Binary files /dev/null and b/garden_planner/lib/assets/plants/pepper.jpg differ diff --git a/garden_planner/lib/assets/plants/raspberries-gce73a006c_640.jpg b/garden_planner/lib/assets/plants/raspberries-gce73a006c_640.jpg new file mode 100644 index 0000000..b952a8f Binary files /dev/null and b/garden_planner/lib/assets/plants/raspberries-gce73a006c_640.jpg differ diff --git a/garden_planner/lib/assets/plants/raspberries-ge56ab3ffc_640.jpg b/garden_planner/lib/assets/plants/raspberries-ge56ab3ffc_640.jpg new file mode 100644 index 0000000..415854c Binary files /dev/null and b/garden_planner/lib/assets/plants/raspberries-ge56ab3ffc_640.jpg differ diff --git a/garden_planner/lib/assets/plants/root-g27af04562_640.jpg b/garden_planner/lib/assets/plants/root-g27af04562_640.jpg new file mode 100644 index 0000000..4fecbf7 Binary files /dev/null and b/garden_planner/lib/assets/plants/root-g27af04562_640.jpg differ diff --git a/garden_planner/lib/assets/plants/salad-seedling-g46a52dd37_640.jpg b/garden_planner/lib/assets/plants/salad-seedling-g46a52dd37_640.jpg new file mode 100644 index 0000000..aca12de Binary files /dev/null and b/garden_planner/lib/assets/plants/salad-seedling-g46a52dd37_640.jpg differ diff --git a/garden_planner/lib/assets/plants/strawberry.jpg b/garden_planner/lib/assets/plants/strawberry.jpg new file mode 100644 index 0000000..e183a9e Binary files /dev/null and b/garden_planner/lib/assets/plants/strawberry.jpg differ diff --git a/garden_planner/lib/assets/plants/tomatoes-gc17bf34c6_640.jpg b/garden_planner/lib/assets/plants/tomatoes-gc17bf34c6_640.jpg new file mode 100644 index 0000000..9241ddd Binary files /dev/null and b/garden_planner/lib/assets/plants/tomatoes-gc17bf34c6_640.jpg differ diff --git a/garden_planner/lib/assets/plants/vertical-water-pipe-64.png b/garden_planner/lib/assets/plants/vertical-water-pipe-64.png new file mode 100644 index 0000000..8c8d1d2 Binary files /dev/null and b/garden_planner/lib/assets/plants/vertical-water-pipe-64.png differ diff --git a/garden_planner/lib/beet.dart b/garden_planner/lib/beet.dart new file mode 100644 index 0000000..af67f6c --- /dev/null +++ b/garden_planner/lib/beet.dart @@ -0,0 +1,50 @@ +import 'plant.dart'; + +class Beet { + List beetrows= [BeetRow()]; + + void Add(BeetRow beetrow){ + beetrows.add(beetrow); + } +} + +class BeetRow { + List plants= []; + + double get verticalSpace { + return getMaxVerticalSpace(this); + } + + double get horizontalSpace { + return plants.map((plant) => plant.horizontalSpace) + .reduce((value, element) => value+=element); + } + + void Add(Plant plant){ + plants.add(plant); + } +} + +double getMaxVerticalSpace(BeetRow beetrow) { + double maxVerticalSpace = 0; + + for (var plant in beetrow.plants) { + if (plant.verticalSpace > maxVerticalSpace) { + maxVerticalSpace = plant.verticalSpace; + } + } + + return maxVerticalSpace; +} + +double getMaxHorizontalSpace(Beet beet) { + double maxHorizontalSpace = 0; + + for (var beetrow in beet.beetrows) { + if (beetrow.horizontalSpace > maxHorizontalSpace) { + maxHorizontalSpace = beetrow.horizontalSpace; + } + } + + return maxHorizontalSpace; +} \ No newline at end of file diff --git a/garden_planner/lib/constance.dart b/garden_planner/lib/constance.dart new file mode 100644 index 0000000..509dd56 --- /dev/null +++ b/garden_planner/lib/constance.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class Constance { + static String get apptitle => 'Garden Beet Planner'; + + static String get apiLocation => 'https://cpd.vierling.cloud'; + + static String get defaultDescription => 'nchts zu tun'; + + static int get maxNumberOfRows => 3; + + static Color get defaultColor => Colors.brown.withOpacity(0.5); + + static bool get sidebarAtStart => true; +} diff --git a/garden_planner/lib/content.dart b/garden_planner/lib/content.dart new file mode 100644 index 0000000..7727104 --- /dev/null +++ b/garden_planner/lib/content.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'beet.dart'; +import 'plant.dart'; + +class Content extends StatefulWidget { + final bool showSpaceRequirement; + + const Content({ + Key? key, + required this.showSpaceRequirement, + }) : super(key: key); + + @override + _ContentState createState() => _ContentState(); +} + +class _ContentState extends State { + Beet beet = Beet(); + + List getRows(Beet beet) { + + List displayedRows = []; + List verticalSpaceContainers = []; + + verticalSpaceContainers=getVerticalSpaceContainers(beet); + + for(int i =0; i getHorizontalSpaceValue(BeetRow beetRow) { + double preUsedSpace = 0; + List spaceElements = []; + + for (var plant in beetRow.plants) { + spaceElements.add(preUsedSpace + (plant.horizontalSpace / 2)); + preUsedSpace += plant.horizontalSpace; + } + + return spaceElements; + } + + List getVerticalSpaceValue(Beet beet) { + double preUsedSpace = 0; + List spaceElements = []; + + for (var rows in beet.beetrows) { + spaceElements.add(preUsedSpace + (rows.verticalSpace / 2)); + preUsedSpace += rows.verticalSpace; + } + + return spaceElements; + } + + Widget getHorizontalSpaceRow(BeetRow beetRow) { + var requiredSpaceValues = getHorizontalSpaceValue(beetRow); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(8), + margin: EdgeInsets.all(4), + height: 100, + width: 150, + color: Colors.green[200], + child: Column( + children: const [ + Text("-") + ] + ), + ), + + for (var item in requiredSpaceValues) + Container( + padding: EdgeInsets.all(8), + margin: EdgeInsets.all(4), + height: 100, + width: 150, + color: Colors.green[200], + child: Column( + children: [ + Text(item.toString()), + ], + ), + ), + ], + ); + } + + List getVerticalSpaceContainers(Beet beet) { + var requiredSpaceValues = getVerticalSpaceValue(beet); + List containers= []; + + for (var item in requiredSpaceValues) { + containers.add( + Container( + padding: EdgeInsets.all(8), + margin: EdgeInsets.all(4), + height: 100, + width: 150, + color: Colors.green[200], + child: Column( + children: [ + Text(item.toString()), + ], + ), + )); + } + + return containers; + } + + Widget getPlantRow(BeetRow beetRow, Widget? verticalSpaceContainers) { + + int plantNumber=0; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if(verticalSpaceContainers!=null) + Container( + padding: EdgeInsets.all(8), + margin: EdgeInsets.all(4), + height: 100, + width: 150, + color: Colors.green[200], + child: Column( + children: [ + verticalSpaceContainers + ] + ), + ), + + for (var plant in beetRow.plants) + Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(4), + height: 100, + width: 150, + color: Colors.green[200], + child: Column( + children: [ + Text(plant.name), + Text('Wasserbedarf: ${plant.waterRequirement}'), + Text('Platz: ${plant.horizontalSpace}'), + ], + ), + ), + DragTarget( + onAccept: (droppedItem) { + setState(() { + beetRow.Add(droppedItem); + }); + }, + builder: (context, candidateData, rejectedData) { + return Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(4), + height: 100, + width: 100, + color: Colors.grey[200], + child: const Center( + child: Text('Drop Plant here'), + ), + ); + }, + ), + ], + ); + } + + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Scrollbar( + thumbVisibility: true, + controller: scrollController, + child: ListView( + scrollDirection: Axis.horizontal, + controller: scrollController, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...getRows(beet), + ElevatedButton( + onPressed: () { + setState(() { + beet.Add(BeetRow()); + }); + }, + child: Text('Neue Reihe'), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/garden_planner/lib/contentcontrol.dart b/garden_planner/lib/contentcontrol.dart new file mode 100644 index 0000000..ef75b7e --- /dev/null +++ b/garden_planner/lib/contentcontrol.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class ContentControl extends StatelessWidget { + final bool showSpaceRequirements; + final ValueChanged onValueChanged; + + ContentControl({super.key, + required this.showSpaceRequirements, + required this.onValueChanged, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text('a'), + Column( + children: [ + Text('Platzbedarf ausblenden'), + Checkbox( + value: showSpaceRequirements, + onChanged: (value) => onValueChanged(value!), + ), + ]), + Text('c'), + ]); + } +} diff --git a/garden_planner/lib/entities/beet.dart b/garden_planner/lib/entities/beet.dart new file mode 100644 index 0000000..5785d89 --- /dev/null +++ b/garden_planner/lib/entities/beet.dart @@ -0,0 +1,15 @@ +import 'package:garden_planner/entities/beet_row.dart'; + +class Beet { + int _internalRow = 0; + List beetRows = []; + + Beet() { + addNewRow(); + } + + void addNewRow() { + beetRows.add(BeetRow(_internalRow)); + _internalRow += 1; + } +} diff --git a/garden_planner/lib/entities/beet_entry_return.dart b/garden_planner/lib/entities/beet_entry_return.dart new file mode 100644 index 0000000..cc15b54 --- /dev/null +++ b/garden_planner/lib/entities/beet_entry_return.dart @@ -0,0 +1,8 @@ +import 'package:garden_planner/entities/plant.dart'; + +class BeetApiEntryReturn { + final Plant plant; + final int beetRow; + + BeetApiEntryReturn(this.plant, this.beetRow); +} diff --git a/garden_planner/lib/entities/beet_row.dart b/garden_planner/lib/entities/beet_row.dart new file mode 100644 index 0000000..d537fed --- /dev/null +++ b/garden_planner/lib/entities/beet_row.dart @@ -0,0 +1,8 @@ +import 'package:garden_planner/entities/plant_in_row.dart'; + +class BeetRow { + int id; + List plants = []; + + BeetRow(this.id); +} diff --git a/garden_planner/lib/entities/plant.dart b/garden_planner/lib/entities/plant.dart new file mode 100644 index 0000000..167b6c5 --- /dev/null +++ b/garden_planner/lib/entities/plant.dart @@ -0,0 +1,29 @@ +import 'package:garden_planner/entities/plant_time.dart'; + +class Plant { + final String name; + final double waterRequirement; + final double horizontalSpace; + final double verticalSpace; + final int id; + + String? image; + String? supType; + List times = []; + + Plant({ + String? imagePath, + List? times, + required this.name, + required this.waterRequirement, + required this.horizontalSpace, + required this.verticalSpace, + required this.id, + required this.supType, + }) { + image = imagePath; + if (times != null) { + this.times.addAll(times); + } + } +} diff --git a/garden_planner/lib/entities/plant_in_row.dart b/garden_planner/lib/entities/plant_in_row.dart new file mode 100644 index 0000000..515e9df --- /dev/null +++ b/garden_planner/lib/entities/plant_in_row.dart @@ -0,0 +1,17 @@ +import 'package:garden_planner/entities/plant.dart'; + +class PlantInRow extends Plant { + final int position; + + PlantInRow({required this.position, required Plant plant}) + : super( + supType: plant.supType, + horizontalSpace: plant.horizontalSpace, + id: plant.id, + name: plant.name, + verticalSpace: plant.verticalSpace, + waterRequirement: plant.waterRequirement) { + image = plant.image; + times = plant.times; + } +} diff --git a/garden_planner/lib/entities/plant_time.dart b/garden_planner/lib/entities/plant_time.dart new file mode 100644 index 0000000..596964e --- /dev/null +++ b/garden_planner/lib/entities/plant_time.dart @@ -0,0 +1,18 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class PlantTime { + final DateTime from; + final DateTime until; + final String description; + final Color color; + final bool action; + + PlantTime( + {required this.color, + required this.description, + required this.from, + required this.until, + required this.action}); +} diff --git a/garden_planner/lib/footer.dart b/garden_planner/lib/footer.dart new file mode 100644 index 0000000..2979718 --- /dev/null +++ b/garden_planner/lib/footer.dart @@ -0,0 +1,59 @@ +import 'package:daydart/daydart.dart'; +import 'package:flutter/material.dart'; + +class Footer extends StatelessWidget { + + final Function(DateTime) onNewDaySelected; + final DateTime date; + + Footer({ + Key? key, + required this.onNewDaySelected, + required this.date + }) : super(key: key); + + get onChanged => {}; + + double GetDayoftheYear(DateTime date){ + + return DayDart(date).dayOfYear().toDouble(); + } + + DateTime GetDatedtime(int dayOfTheYear){ + var currentYear = DayDart().year(); + + DayDart date = DayDart('$currentYear-01-01'); + date.add(dayOfTheYear-1,DayUnits.D); + + return date.toDate(); + } + + @override + Widget build(BuildContext context) { + return Row( + + children: [ + Container(), + Container( + child: Column( + children: [ + Text(GetDatedtime(GetDayoftheYear(date).toInt()).toString()), + Slider( + value: GetDayoftheYear(date), + min: 1, + max: 365, + onChanged: (value) { + onNewDaySelected(GetDatedtime(value.toInt())); + }, + ), + ], + ), + + ), + Container() + ], + ); + + + } +} \ No newline at end of file diff --git a/garden_planner/lib/header.dart b/garden_planner/lib/header.dart new file mode 100644 index 0000000..e4f5360 --- /dev/null +++ b/garden_planner/lib/header.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class Header extends StatelessWidget implements PreferredSizeWidget { + final String title; + final Function() onSidebarToggle; + + const Header({Key? key, required this.title, required this.onSidebarToggle}) + : super(key: key); + + @override + Size get preferredSize => Size.fromHeight(60); + + @override + Widget build(BuildContext context) { + return AppBar( + title: Text(title), + actions: [ + IconButton( + icon: Icon(Icons.menu), + onPressed: () { + onSidebarToggle(); // Einblenden der Sidebar + }, + ), + ], + ); + } +} diff --git a/garden_planner/lib/logic/beet.service.dart b/garden_planner/lib/logic/beet.service.dart new file mode 100644 index 0000000..3177976 --- /dev/null +++ b/garden_planner/lib/logic/beet.service.dart @@ -0,0 +1,83 @@ +import 'package:garden_planner/api/garden_api.service.dart'; +import 'package:garden_planner/entities/beet.dart'; +import 'package:garden_planner/entities/beet_entry_return.dart'; +import 'package:garden_planner/entities/beet_row.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/logic/beet_row.service.dart'; +import 'package:garden_planner/logic/plant.service.dart'; + +class BeetService { + void addPlantToRowById(final Beet beet, final BeetRowService rowService, + final int rowId, final Plant plant) { + final BeetRow row = rowService.getRowById(beet, rowId); + rowService.addPlant(row, plant); + } + + void addNewRow(final Beet beet) { + beet.beetRows.add(BeetRow(beet.beetRows.length)); + } + + double getMaxHorizontalSpaceOfRows( + final BeetRowService beetRowService, final Beet beet) { + double maxHorizontalSpace = 0; + + for (final BeetRow beetRow in beet.beetRows) { + final double horizontalSpace = + beetRowService.getTotalHorizontalSpace(beetRow); + + if (horizontalSpace > maxHorizontalSpace) { + maxHorizontalSpace = horizontalSpace; + } + } + + return maxHorizontalSpace; + } + + bool isActionNeeded(final BeetRowService beetRowService, + final PlantService plantService, final Beet beet, final DateTime date) { + final bool isActionNeeded = beet.beetRows.any((final BeetRow row) => + beetRowService.isActionNeeded(plantService, row, date)); + + return isActionNeeded; + } + + List getVerticalPlantingSpace( + final BeetRowService beetRowService, final Beet beet) { + double preUsedSpace = 0; + final List spaceElements = []; + + for (final BeetRow row in beet.beetRows) { + final double maxVerticalSpace = + beetRowService.getMaxVerticalSpaceOfPlantInTheRow(row); + var position = preUsedSpace + (maxVerticalSpace / 2); + + spaceElements.add(position); + preUsedSpace += maxVerticalSpace; + } + + return spaceElements; + } + + Future saveBeet( + final GardenApiService apiService, final Beet beet) async { + return apiService.saveBeet(beet); + } + + Future loadBeet(final GardenApiService apiService, + final BeetRowService rowService, final Beet beet) async { + final List beetEntries = await apiService.getBeet(); + + for (final BeetApiEntryReturn beetEntry in beetEntries) { + _addLoadedPlant(beet, rowService, beetEntry.plant, beetEntry.beetRow); + } + } + + void _addLoadedPlant(final Beet beet, final BeetRowService rowService, + final Plant plant, final int row) { + while (beet.beetRows.length <= row) { + addNewRow(beet); + } + + rowService.addPlant(beet.beetRows[row], plant); + } +} diff --git a/garden_planner/lib/logic/beet_row.service.dart b/garden_planner/lib/logic/beet_row.service.dart new file mode 100644 index 0000000..e842494 --- /dev/null +++ b/garden_planner/lib/logic/beet_row.service.dart @@ -0,0 +1,64 @@ +import 'package:garden_planner/entities/beet.dart'; +import 'package:garden_planner/entities/beet_row.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/entities/plant_in_row.dart'; +import 'package:garden_planner/logic/plant.service.dart'; + +class BeetRowService { + double getTotalHorizontalSpace(final BeetRow beetRow) { + return beetRow.plants + .map((plant) => plant.horizontalSpace) + .reduce((value, element) => value + element); + } + + double getMaxVerticalSpaceOfPlantInTheRow(final BeetRow beetRow) { + double maxVerticalSpace = 0; + + for (var plant in beetRow.plants) { + if (plant.verticalSpace > maxVerticalSpace) { + maxVerticalSpace = plant.verticalSpace; + } + } + + return maxVerticalSpace; + } + + bool isActionNeeded(final PlantService plantService, final BeetRow beetRow, + final DateTime date) { + bool isActionNeeded = + beetRow.plants.any((plant) => plantService.isActionNeeded(plant, date)); + + return isActionNeeded; + } + + List getHorizontalPlantingPosition(final BeetRow beetRow) { + double preUsedSpace = 0; + List spaceElements = []; + + for (var plant in beetRow.plants) { + double position = preUsedSpace + (plant.horizontalSpace / 2); + + spaceElements.add(position); + preUsedSpace += plant.horizontalSpace; + } + + return spaceElements; + } + + BeetRow getRowById(final Beet beet, final int rowId) { + return beet.beetRows.firstWhere((element) => element.id == rowId); + } + + void removePlantFromRowById(final BeetRow beetRow, final int position) { + beetRow.plants.removeWhere((plant) => plant.position == position); + } + + void addPlant(final BeetRow row, final Plant plant) { + row.plants.add(_generatePlantInRow(row, plant)); + } + + PlantInRow _generatePlantInRow(final BeetRow row, final Plant plant) { + var numberOfCurrentPlant = row.plants.length; + return PlantInRow(position: numberOfCurrentPlant, plant: plant); + } +} diff --git a/garden_planner/lib/logic/date.helper.dart b/garden_planner/lib/logic/date.helper.dart new file mode 100644 index 0000000..8894c11 --- /dev/null +++ b/garden_planner/lib/logic/date.helper.dart @@ -0,0 +1,37 @@ +import 'package:daydart/daydart.dart'; + +class DateHelper { + //ignores th year an moves the Dates every time to the current year + static bool isDateBetween( + final DateTime date, final DateTime from, final DateTime until) { + final currentDate = transformToCurrentYear(date); + final currentFrom = transformToCurrentYear(from); + final currentUntil = transformToCurrentYear(until); + + var isBetween = (currentDate.isAtSameMomentAs(currentFrom) || + currentDate.isAfter(currentFrom)) && + (currentDate.isAtSameMomentAs(currentUntil) || + currentDate.isBefore(currentUntil)); + + return isBetween; + } + + static DateTime getDateTimeByDayOfYear(final int dayOfTheYear) { + final currentYear = DayDart().year(); + final date = DayDart('$currentYear-01-01'); + date.add(dayOfTheYear - 1, DayUnits.D); + + return date.toDate(); + } + + static int getDayOfYear(final DateTime date) { + return DayDart(date).dayOfYear(); + } + + static DateTime transformToCurrentYear(final DateTime date) { + final currentYear = DateTime.now().year; + final newDate = DateTime(currentYear, date.month, date.day); + + return newDate; + } +} diff --git a/garden_planner/lib/logic/plant.service.dart b/garden_planner/lib/logic/plant.service.dart new file mode 100644 index 0000000..f3b65d7 --- /dev/null +++ b/garden_planner/lib/logic/plant.service.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:garden_planner/constance.dart'; + +import '../entities/plant.dart'; +import '../entities/plant_time.dart'; +import 'date.helper.dart'; + +class PlantService { + String getTimeDescription(final Plant plant, final DateTime date) { + var plantTimes = _getPlantTimes(plant, date); + if (plantTimes.isNotEmpty) { + return plantTimes.first.description; + } + + return Constance.defaultDescription; + } + + Color getColor(final Plant plant, final DateTime date) { + Color color = Constance.defaultColor; + + if (plant.times.isNotEmpty) { + for (var time in plant.times) { + if (DateHelper.isDateBetween(date, time.from, time.until)) { + color = time.color; + break; + } + } + } + + return color; + } + + bool isActionNeeded(final Plant plant, final DateTime date) { + return _getPlantTimes(plant, date).any((time) => time.action); + } + + List _getPlantTimes(final Plant plant, final DateTime date) { + return plant.times + .where((time) => DateHelper.isDateBetween(date, time.from, time.until)) + .toList(); + } +} diff --git a/garden_planner/lib/main.dart b/garden_planner/lib/main.dart new file mode 100644 index 0000000..abee66a --- /dev/null +++ b/garden_planner/lib/main.dart @@ -0,0 +1,191 @@ +<<<<<<< refs/remotes/origin/main +import 'package:flutter/material.dart'; +import 'package:garden_planner/constance.dart'; + +import 'api/garden_api.service.dart'; +import 'api/http_connection.dart'; +import 'logic/beet.service.dart'; +import 'logic/beet_row.service.dart'; +import 'logic/plant.service.dart'; +import 'repositories/beet.repositories.dart'; +import 'widgets/content.dart'; +import 'widgets/header.dart'; +import 'widgets/sidebar.dart'; + +void main() { + BeetRepository beetRepository = BeetRepository( + BeetRowService(), + PlantService(), + GardenApiService(HttpConnector()), + BeetService(), + ); + + + runApp(GardenPlanner(beetRepository: beetRepository)); +} + +class GardenPlanner extends StatefulWidget { + final BeetRepository beetRepository; + + const GardenPlanner({super.key, required this.beetRepository}); + + @override + GardenPlannerState createState() => GardenPlannerState(); +} + +class GardenPlannerState extends State { + bool _isSidebarOpen = Constance.sidebarAtStart; + + @override + void initState() { + super.initState(); + loadBeet(); + } + + Future loadBeet() async { + await widget.beetRepository.loadBeet(); + setState(() {}); + } + + Future saveBeet() async { + await widget.beetRepository.saveBeet(); + setState(() {}); + } + + void toogleSidebar() { + setState(() { + _isSidebarOpen = !_isSidebarOpen; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Garden Planner', + theme: ThemeData( + primarySwatch: Colors.green, + ), + home: Scaffold( + appBar: Header( + onSidebarToggle: toogleSidebar, + onSave: saveBeet, + ), + body: Row( + children: [ + if (_isSidebarOpen) + Container( + width: 250, + margin: const EdgeInsets.only(top: 10, bottom: 10), + decoration: _getDecorator(), + padding: const EdgeInsets.only(right: 5, left: 5), + child: Sidebar(beetRepository: widget.beetRepository), + ), + Expanded( + child: Content(beetRepository: widget.beetRepository), + ), + ], + ), + ), + ); + } + + BoxDecoration _getDecorator() { + return const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.green, + Colors.green, + ], + ), + borderRadius: BorderRadius.only( + topRight: Radius.circular(40), + bottomRight: Radius.circular(0), + ), + ); + } +} +======= +import 'package:daydart/daydart.dart'; +import 'package:flutter/material.dart'; +import 'contentcontrol.dart'; +import 'footer.dart'; +import 'header.dart'; +import 'content.dart'; +import 'sidebar.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Drag and Drop Beispiel', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: DragAndDropScreen(), + ); + } +} + +class DragAndDropScreen extends StatefulWidget { + @override + _DragAndDropScreenState createState() => _DragAndDropScreenState(); +} + +class _DragAndDropScreenState extends State { + bool showSpaceRequirements = false; + bool isSidebarOpen= true; + DateTime selectedDate=DateTime.now(); + + void toggleSidebar() { + setState(() { + isSidebarOpen = !isSidebarOpen; + }); + } + + void newDaySelected(DateTime date) { + setState(() { + this.selectedDate=date; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: Header(title: 'Drag and Drop Beispiel', + onSidebarToggle: toggleSidebar,), + body: Row( + children: [ + if (isSidebarOpen) Sidebar(), + Expanded( + child: Column( + children: [ + ContentControl( + showSpaceRequirements: showSpaceRequirements, + onValueChanged: (value) { + setState(() { + showSpaceRequirements = value; + }); + }, + ), + + Content( + showSpaceRequirement: showSpaceRequirements, + ), + Expanded( + child: Footer(onNewDaySelected:newDaySelected,date: selectedDate), + ) + ], + ), + ), + ], + ), + ); + } +} +>>>>>>> working diff --git a/garden_planner/lib/plant.dart b/garden_planner/lib/plant.dart new file mode 100644 index 0000000..5ff952c --- /dev/null +++ b/garden_planner/lib/plant.dart @@ -0,0 +1,13 @@ +class Plant { + final String name; + final double waterRequirement; + final double horizontalSpace; + final double verticalSpace; + + Plant({ + required this.name, + required this.waterRequirement, + required this.horizontalSpace, + required this.verticalSpace, + }); +} diff --git a/garden_planner/lib/repositories/beet.repositories.dart b/garden_planner/lib/repositories/beet.repositories.dart new file mode 100644 index 0000000..042c2e4 --- /dev/null +++ b/garden_planner/lib/repositories/beet.repositories.dart @@ -0,0 +1,82 @@ +import 'package:garden_planner/api/garden_api.service.dart'; +import 'package:garden_planner/constance.dart'; +import 'package:garden_planner/entities/beet.dart'; +import 'package:garden_planner/entities/beet_row.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/logic/beet.service.dart'; +import 'package:garden_planner/logic/beet_row.service.dart'; +import 'package:garden_planner/logic/plant.service.dart'; + +class BeetRepository { + late Beet beet; + final BeetService beetService; + final BeetRowService beetRowService; + final PlantService plantService; + final GardenApiService apiService; + + late String _messages; + + BeetRepository(this.beetRowService, this.plantService, this.apiService, + this.beetService) { + beet = Beet(); + _messages = "Keine Meldungen"; + } + + String get messages => _messages; + + bool get newRowAllowed => beet.beetRows.length < Constance.maxNumberOfRows; + + void addPlantToRowById(int rowId, Plant plant) { + beetService.addPlantToRowById(beet, beetRowService, rowId, plant); + } + + void addNewRowToBeet() { + beetService.addNewRow(beet); + } + + double getMaxHorizontalSpace() { + return beetService.getMaxHorizontalSpaceOfRows(beetRowService, beet); + } + + bool isActionNeeded(DateTime date) { + return beetService.isActionNeeded(beetRowService, plantService, beet, date); + } + + List getVerticalSpaceValue() { + return beetService.getVerticalPlantingSpace(beetRowService, beet); + } + + void removePlantFromRowById(int rowId, int plantInRowId) { + var row = beetRowService.getRowById(beet, rowId); + beetRowService.removePlantFromRowById(row, plantInRowId); + } + + Future saveBeet() async { + _messages = await beetService.saveBeet(apiService, beet); + } + + Future> getAllPlants() async { + return apiService.getAllAvailablePlants(); + } + + getBackgroundColorOfPlant(Plant plant, DateTime date) { + return plantService.getColor(plant, date); + } + + String getTimeDescription(Plant plant, DateTime date) { + return plantService.getTimeDescription(plant, date); + } + + Future loadBeet() async { + await beetService.loadBeet(apiService, beetRowService, beet); + _messages = "Beet geladen"; + } + + List getHorizontalPlantingPositionForRow(BeetRow beetRow) { + return beetRowService.getHorizontalPlantingPosition(beetRow); + } + + BeetRow getRow(int index) { + return beet.beetRows[index]; + } +} diff --git a/garden_planner/lib/sidebar.dart b/garden_planner/lib/sidebar.dart new file mode 100644 index 0000000..9c06f52 --- /dev/null +++ b/garden_planner/lib/sidebar.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import 'plant.dart'; + +class Sidebar extends StatelessWidget { + List sidebarItems = [ + Plant( + name: 'Pflanze 1', + waterRequirement: 3, + horizontalSpace: 2, + verticalSpace: 2, + ), + Plant( + name: 'Pflanze 2', + waterRequirement: 5, + horizontalSpace: 1, + verticalSpace: 3, + ), + Plant( + name: 'Pflanze 3', + waterRequirement: 2, + horizontalSpace: 3, + verticalSpace: 1, + ), + ]; + + Sidebar({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var plant in sidebarItems) + Draggable( + data: plant, + child: Container( + padding: EdgeInsets.all(8), + margin: EdgeInsets.all(4), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(plant.name), + Text('Wasserbedarf: ${plant.waterRequirement}'), + Text('Platzbedarf: ${plant.horizontalSpace} x ${plant + .verticalSpace}'), + ], + ), + ), + feedback: Container( + padding: EdgeInsets.all(8), + margin: EdgeInsets.all(4), + color: Colors.blue[200], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(plant.name), + Text('Wasserbedarf: ${plant.waterRequirement}'), + Text('Platzbedarf: ${plant.horizontalSpace} x ${plant + .verticalSpace}'), + ], + ), + ), + childWhenDragging: Container(), + ) + ] + ); + } +} diff --git a/garden_planner/lib/widgets/content.dart b/garden_planner/lib/widgets/content.dart new file mode 100644 index 0000000..5541608 --- /dev/null +++ b/garden_planner/lib/widgets/content.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +import '../entities/plant.dart'; +import '../entities/plant_in_row.dart'; +import '../repositories/beet.repositories.dart'; +import 'content_widgets/control_bar.dart'; +import 'content_widgets/dashboard.dart'; +import 'content_widgets/footer.dart'; +import 'content_widgets/new_beet_row.dart'; + +class Content extends StatefulWidget { + final BeetRepository beetRepository; + + const Content({required this.beetRepository, Key? key}) : super(key: key); + + @override + ContentState createState() => ContentState(); +} + +class ContentState extends State { + bool showSpaceRequirements = false; + bool showImages = false; + bool isSidebarOpen = true; + DateTime selectedDate = DateTime.now(); + + @override + void initState() { + super.initState(); + } + + void toggleSidebar() { + setState(() { + isSidebarOpen = !isSidebarOpen; + }); + } + + void saveBeet() { + setState(() { + widget.beetRepository.saveBeet(); + }); + } + + void plantDroppedOnRow(int rowId, Plant plant) { + setState(() { + widget.beetRepository.addPlantToRowById(rowId, plant); + }); + } + + void newRow() { + setState(() { + widget.beetRepository.addNewRowToBeet(); + }); + } + + void newDaySelected(DateTime date) { + setState(() { + selectedDate = date; + }); + } + + void plantRemovedFromRow(int rowId, PlantInRow plantInRow) { + setState(() { + widget.beetRepository.removePlantFromRowById(rowId, plantInRow.position); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Control( + showSpaceRequirements: showSpaceRequirements, + onShowSpaceChanged: (value) { + setState(() { + showSpaceRequirements = value; + }); + }, + showImages: showImages, + onImagesChanged: (value) { + setState(() { + showImages = value; + }); + }, + actionIsNeeded: widget.beetRepository.isActionNeeded(selectedDate), + ), + Expanded( + child: Column(children: [ + Dashboard( + onPlantDroppedToRow: plantDroppedOnRow, + beetRepository: widget.beetRepository, + showSpaceRequirement: showSpaceRequirements, + showImages: showImages, + currentDate: selectedDate, + onPlantRemoveFromRow: plantRemovedFromRow), + if (widget.beetRepository.newRowAllowed) NewBeetRow(onNewRow: newRow) + ])), + Footer( + onNewDaySelected: newDaySelected, + beetRepository: widget.beetRepository, + date: selectedDate, + ) + ], + ); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/control_bar.dart b/garden_planner/lib/widgets/content_widgets/control_bar.dart new file mode 100644 index 0000000..caad499 --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/control_bar.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; + +class Control extends StatelessWidget { + final bool showSpaceRequirements; + final Function(bool) onShowSpaceChanged; + + final bool showImages; + final Function(bool) onImagesChanged; + final bool actionIsNeeded; + + const Control({ + Key? key, + required this.showSpaceRequirements, + required this.onShowSpaceChanged, + required this.showImages, + required this.onImagesChanged, + required this.actionIsNeeded, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(10), + padding: const EdgeInsets.only(left: 1, right: 1, top: 10, bottom: 10), + decoration: _getDecoration(), + child: LayoutBuilder( + builder: (context, constraints) { + final smallSpace = constraints.maxWidth < 300; + + if (smallSpace) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: _getControlElements(smallSpace), + ); + } else { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: _getControlElements(smallSpace), + ); + } + }, + ), + ); + } + + List _getControlElements(bool reducedSpace) { + return [ + if (actionIsNeeded) + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Icon(Icons.warning), + if (!reducedSpace) const Text("Aktion nötig"), + ], + ), + if (!reducedSpace && !actionIsNeeded) + const Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text("Nichts zu tun"), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('Bilder / Text'), + Checkbox( + value: showImages, + onChanged: (value) => onImagesChanged(value ?? false), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('Pflanzposition'), + Checkbox( + value: showSpaceRequirements, + onChanged: (value) => onShowSpaceChanged(value ?? false), + ), + ], + ), + ]; + } + + BoxDecoration _getDecoration() { + return const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.topRight, + colors: [ + Colors.green, + Colors.yellow, + ], + ), + borderRadius: BorderRadius.all(Radius.circular(40)), + ); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/dashboard.dart b/garden_planner/lib/widgets/content_widgets/dashboard.dart new file mode 100644 index 0000000..3528715 --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/dashboard.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +import '../../entities/plant.dart'; +import '../../entities/plant_in_row.dart'; +import '../../repositories/beet.repositories.dart'; +import 'dashboard_widgets/plant_row.dart'; +import 'dashboard_widgets/space/plant_row_horizontal_space.dart'; + +class Dashboard extends StatelessWidget { + final bool showSpaceRequirement; + final DateTime currentDate; + final Function(int, Plant) onPlantDroppedToRow; + final Function(int, PlantInRow) onPlantRemoveFromRow; + final bool showImages; + final BeetRepository beetRepository; + + const Dashboard({ + Key? key, + required this.onPlantDroppedToRow, + required this.beetRepository, + required this.showSpaceRequirement, + required this.showImages, + required this.currentDate, + required this.onPlantRemoveFromRow, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var verticalSpace = beetRepository.getVerticalSpaceValue(); + + return Container( + alignment: Alignment.topLeft, + margin: const EdgeInsets.only(left: 10), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int rowIndex = 0; + rowIndex < beetRepository.beet.beetRows.length; + rowIndex++) + SizedBox( + height: showSpaceRequirement && + beetRepository.getRow(rowIndex).plants.isNotEmpty + ? 170 + : 120, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showSpaceRequirement && + beetRepository.getRow(rowIndex).plants.isNotEmpty) + PlantRowHorizontalSpace( + plantingPositions: + beetRepository.getHorizontalPlantingPositionForRow( + beetRepository.getRow(rowIndex)), + ), + PlantRow( + showSpaceRequirement: showSpaceRequirement, + row: beetRepository.getRow(rowIndex), + verticalSpace: verticalSpace[rowIndex], + beetRepository: beetRepository, + showImages: showImages, + date: currentDate, + onPlantRemove: (PlantInRow plantInRow) { + onPlantRemoveFromRow( + beetRepository.getRow(rowIndex).id, + plantInRow, + ); + }, + onPlantDropped: (Plant plant) { + onPlantDroppedToRow( + beetRepository.getRow(rowIndex).id, + plant, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_drop.dart b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_drop.dart new file mode 100644 index 0000000..2995161 --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_drop.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import '../../../../entities/plant.dart'; + +class PlantDrop extends StatelessWidget { + final Function(Plant) onPlantDropped; + final bool showSpaceRequirement; + + const PlantDrop({ + Key? key, + required this.onPlantDropped, + required this.showSpaceRequirement, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DragTarget( + onAccept: (droppedItem) { + onPlantDropped(droppedItem); + }, + builder: (context, candidateData, rejectedData) { + return Container( + margin: const EdgeInsets.all(0), + height: showSpaceRequirement ? 100 : 100, + width: 100, + decoration: BoxDecoration( + color: Colors.brown, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'lib/assets/layout/planting-64-white.png', + ), + const Expanded( + child: Text('Pflanzen', + style: TextStyle( + color: Colors.white, + )), + ), + ], + ), + ); + }, + ); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_element.dart b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_element.dart new file mode 100644 index 0000000..269199b --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_element.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +import '../../../../entities/plant_in_row.dart'; +import '../../../../repositories/beet.repositories.dart'; + +class PlantElement extends StatelessWidget { + final bool showImages; + final Function(PlantInRow) onRemovePlant; + final PlantInRow plant; + final BeetRepository beetRepository; + final DateTime date; + + const PlantElement({ + Key? key, + required this.showImages, + required this.onRemovePlant, + required this.plant, + required this.beetRepository, + required this.date, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(5), + padding: const EdgeInsets.all(10), + height: 100, + width: 160, + decoration: BoxDecoration( + color: beetRepository.getBackgroundColorOfPlant(plant, date), + borderRadius: BorderRadius.circular(8), + ), + child: showImages ? _buildImageContent() : _buildTextContent(), + ); + } + + Widget _buildTextContent() { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + plant.name, + textAlign: TextAlign.center, + ), + const Divider(), + Text(beetRepository.getTimeDescription(plant, date)), + Expanded(child: _buildDropElement()), + ], + ); + } + + Widget _buildImageContent() { + return Row( + children: [ + Expanded(child: Image.asset(plant.image ?? '')), + _buildDropElement(), + ], + ); + } + + Widget _buildDropElement() { + return IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + onRemovePlant(plant); + }, + ); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_row.dart b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_row.dart new file mode 100644 index 0000000..140d3c6 --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/plant_row.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import '../../../../entities/beet_row.dart'; +import '../../../../entities/plant.dart'; +import '../../../../entities/plant_in_row.dart'; +import '../../../../repositories/beet.repositories.dart'; +import 'plant_drop.dart'; +import 'plant_element.dart'; +import 'space/plant_row_space.dart'; + +class PlantRow extends StatelessWidget { + final Function(Plant) onPlantDropped; + final Function(PlantInRow) onPlantRemove; + final bool showSpaceRequirement; + final double verticalSpace; + final BeetRow row; + final bool showImages; + final BeetRepository beetRepository; + final DateTime date; + + const PlantRow( + {Key? key, + required this.row, + required this.verticalSpace, + required this.onPlantDropped, + required this.showSpaceRequirement, + required this.beetRepository, + required this.onPlantRemove, + required this.showImages, + required this.date}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row(children: [ + if (showSpaceRequirement && row.plants.isNotEmpty) + PlantRowSpace(verticalSpace: verticalSpace), + for (PlantInRow plant in row.plants) + PlantElement( + showImages: showImages, + onRemovePlant: onPlantRemove, + plant: plant, + beetRepository: beetRepository, + date: date), + PlantDrop( + showSpaceRequirement: showSpaceRequirement, + onPlantDropped: onPlantDropped) + ]); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/dashboard_widgets/space/plant_row_horizontal_space.dart b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/space/plant_row_horizontal_space.dart new file mode 100644 index 0000000..3bc1e4f --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/space/plant_row_horizontal_space.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +class PlantRowHorizontalSpace extends StatelessWidget { + final List plantingPositions; + + const PlantRowHorizontalSpace({ + Key? key, + required this.plantingPositions, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + _buildCross(), + ...plantingPositions + .map((position) => _buildPlantingPosition(position)), + ], + ); + } + + Widget _buildCross() { + return Container( + margin: const EdgeInsets.all(5), + height: 50, + width: 100, + decoration: BoxDecoration( + color: Colors.brown, + borderRadius: BorderRadius.circular(8), + ), + child: const Center(), + ); + } + + Widget _buildPlantingPosition(double position) { + return Container( + margin: const EdgeInsets.all(5), + height: 50, + width: 160, + decoration: BoxDecoration( + color: Colors.brown, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'lib/assets/layout/width-64.png', + height: 32, + width: 32, + ), + Text( + '${position.toStringAsFixed(2)} m', + style: const TextStyle(color: Colors.white), + ), + ], + ), + ); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/dashboard_widgets/space/plant_row_space.dart b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/space/plant_row_space.dart new file mode 100644 index 0000000..9449446 --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/space/plant_row_space.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class PlantRowSpace extends StatelessWidget { + final double verticalSpace; + + const PlantRowSpace({ + Key? key, + required this.verticalSpace, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(5), + height: 100, + width: 100, + decoration: BoxDecoration( + color: Colors.brown, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'lib/assets/layout/height-64.png', + height: 32, + width: 32, + ), + Text( + '${verticalSpace.toStringAsFixed(2)} m', + style: const TextStyle(color: Colors.white), + ), + ], + ), + ); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/dashboard_widgets/water/plant_row_horizontal_water.dart b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/water/plant_row_horizontal_water.dart new file mode 100644 index 0000000..f30dcf4 --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/water/plant_row_horizontal_water.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class PlantRowHorizontalWater extends StatelessWidget { + final List waterConsumption; + + const PlantRowHorizontalWater({ + Key? key, + required this.waterConsumption, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row(children: [ + for (var item in waterConsumption) + Container( + margin: const EdgeInsets.all(5), + height: 50, + width: 160, + color: Colors.brown, + child: Flex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'lib/assets/layout/horizontal-water-pipe-64.png', + ), + Text("$item m", style: const TextStyle(color: Colors.white)), + ], + ), + ) + ]); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/dashboard_widgets/water/plant_row_water.dart b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/water/plant_row_water.dart new file mode 100644 index 0000000..46a5f41 --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/dashboard_widgets/water/plant_row_water.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class PlantRowWater extends StatelessWidget { + final double waterRequirment; + + const PlantRowWater({Key? key, required this.waterRequirment}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(5), + height: 100, + width: 100, + color: Colors.blue, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('lib/assets/layout/vertical-water-pipe-64.png'), + Text("$waterRequirment l", + style: const TextStyle(color: Colors.white)), + ], + ), + ); + } +} diff --git a/garden_planner/lib/widgets/content_widgets/footer.dart b/garden_planner/lib/widgets/content_widgets/footer.dart new file mode 100644 index 0000000..b8258ed --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/footer.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../logic/date.helper.dart'; +import '../../repositories/beet.repositories.dart'; + +class Footer extends StatelessWidget { + final Function(DateTime) onNewDaySelected; + final BeetRepository beetRepository; + final DateTime date; + + const Footer({ + required this.beetRepository, + required this.date, + required this.onNewDaySelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column(children: [ + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + IconButton( + icon: const Icon(Icons.calendar_today), + onPressed: () { + _showDatePicker(context); + }, + ), + const SizedBox(width: 10), + Text(DateFormat('dd.MM.yyyy').format( + DateHelper.transformToCurrentYear(date), + )) + ]), + Slider( + value: DateHelper.getDayOfYear(date).toDouble(), + min: 1, + max: 365, + activeColor: Colors.green, + inactiveColor: Colors.lightGreen, + onChanged: (value) { + onNewDaySelected( + DateHelper.getDateTimeByDayOfYear(value.toInt()), + ); + }) + ]); + } + + void _showDatePicker(BuildContext context) async { + final selectedDate = await showDatePicker( + context: context, + initialDate: date, + firstDate: DateTime(DateTime.now().year), + lastDate: DateTime(DateTime.now().year + 1), + ); + + if (selectedDate != null) { + onNewDaySelected(selectedDate); + } + } +} diff --git a/garden_planner/lib/widgets/content_widgets/new_beet_row.dart b/garden_planner/lib/widgets/content_widgets/new_beet_row.dart new file mode 100644 index 0000000..71dbaaf --- /dev/null +++ b/garden_planner/lib/widgets/content_widgets/new_beet_row.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class NewBeetRow extends StatelessWidget { + final VoidCallback onNewRow; + + const NewBeetRow({Key? key, required this.onNewRow}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(20), + alignment: Alignment.topLeft, + child: ElevatedButton( + onPressed: onNewRow, + child: const Text('Neue Reihe'), + ), + ); + } +} diff --git a/garden_planner/lib/widgets/header.dart b/garden_planner/lib/widgets/header.dart new file mode 100644 index 0000000..fccdb15 --- /dev/null +++ b/garden_planner/lib/widgets/header.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:garden_planner/constance.dart'; + +class Header extends StatelessWidget implements PreferredSizeWidget { + final Function() onSidebarToggle; + final Function() onSave; + + const Header({Key? key, required this.onSidebarToggle, required this.onSave}) + : super(key: key); + + @override + Size get preferredSize => const Size.fromHeight(60); + + @override + Widget build(BuildContext context) { + return AppBar( + leading: IconButton( + icon: const Icon(Icons.menu_open), + onPressed: () { + onSidebarToggle(); + }, + ), + title: Text(Constance.apptitle), + actions: [ +Container( + margin: const EdgeInsets.all(5), + child: + ElevatedButton.icon( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Bestätigen'), + content: const Text('Möchten Sie speichern?'), + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Abbrechen'), + ), + ElevatedButton( + child: const Text('Ja'), + onPressed: () { + onSave(); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + }, + icon: const Icon(Icons.save), + label: const Text('Speichern'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.black, + backgroundColor: Colors.white, + ), + ) +) + ], + ); + } +} diff --git a/garden_planner/lib/widgets/sidebar.dart b/garden_planner/lib/widgets/sidebar.dart new file mode 100644 index 0000000..afb6a29 --- /dev/null +++ b/garden_planner/lib/widgets/sidebar.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; + +import '../entities/plant.dart'; +import '../repositories/beet.repositories.dart'; + +class Sidebar extends StatelessWidget { + final BeetRepository beetRepository; + + const Sidebar({required this.beetRepository, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + margin: const EdgeInsets.only(top: 20), + padding: const EdgeInsets.all(15), + child: const Text( + "Verfügbare Pflanzen", + style: TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), + ), + Container( + padding: const EdgeInsets.all(15), + child: const Text( + "Platziere das Bild auf dem Beet", + textDirection: TextDirection.ltr, + style: TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + ), + const Divider(), + Expanded( + child: FutureBuilder>( + future: beetRepository.getAllPlants(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return const Text("Fehler beim Laden"); + } else if (snapshot.hasData) { + final plants = snapshot.data!; + + return Scrollbar( + child: ListView.builder( + scrollDirection: Axis.vertical, + itemCount: plants.length, + itemBuilder: (context, index) { + final plant = plants.elementAt(index); + return Container( + padding: const EdgeInsets.all(5), + margin: const EdgeInsets.only( + bottom: 10, + top: 10, + right: 10, + ), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Draggable( + data: plant, + feedback: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + plant.image.toString(), + width: 100, + ), + ], + ), + ), + child: Image.asset( + plant.image.toString(), + width: 100, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only( + right: 8, + left: 8, + top: 8, + ), + child: Text( + '${plant.name} \n ${plant.supType ?? ''}', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Container( + padding: const EdgeInsets.all(8), + child: Text( + '${plant.horizontalSpace} x ${plant.verticalSpace} m', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ); + }, + ), + ); + } else if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + return const Text('Keine Pflanzen gefunden'); + } + }, + ), + ), + ], + ); + } +} diff --git a/garden_planner/linux/.gitignore b/garden_planner/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/garden_planner/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/garden_planner/linux/CMakeLists.txt b/garden_planner/linux/CMakeLists.txt new file mode 100644 index 0000000..f304a76 --- /dev/null +++ b/garden_planner/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "garden_planner") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.garden_planner") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/garden_planner/linux/flutter/CMakeLists.txt b/garden_planner/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/garden_planner/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/garden_planner/linux/main.cc b/garden_planner/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/garden_planner/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/garden_planner/linux/my_application.cc b/garden_planner/linux/my_application.cc new file mode 100644 index 0000000..fe2f6fd --- /dev/null +++ b/garden_planner/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "garden_planner"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "garden_planner"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/garden_planner/linux/my_application.h b/garden_planner/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/garden_planner/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/garden_planner/pubspec.yaml b/garden_planner/pubspec.yaml new file mode 100644 index 0000000..8a2ada9 --- /dev/null +++ b/garden_planner/pubspec.yaml @@ -0,0 +1,30 @@ +name: garden_planner +description: App zum Planen des Beets mit der passenden Position und Länge der jeweiligen Leitung für die dazugehörige Wasserleitung + +version: 1.0.0+1 + +environment: + sdk: '>=2.19.6 <3.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + daydart: ^0.0.5 + postgres: ^2.1.0 + http: ^0.13.3 + intl: ^0.17.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + + + + flutter_lints: ^2.0.1 +flutter: + uses-material-design: true + assets: + - lib/assets/layout/ + - lib/assets/plants/ \ No newline at end of file diff --git a/garden_planner/test/api/api_entities/beet_entry_return_test.dart b/garden_planner/test/api/api_entities/beet_entry_return_test.dart new file mode 100644 index 0000000..bbc16c0 --- /dev/null +++ b/garden_planner/test/api/api_entities/beet_entry_return_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/beet_entry_return.dart'; +import 'package:garden_planner/entities/plant.dart'; + +void main() { + test('beetEntryReturn entity Test', () { + // Arrange + Plant plant = Plant( + name: "name", + waterRequirement: 1, + horizontalSpace: 2, + verticalSpace: 3, + id: 4, + supType: "description"); + const int beetRow = 5; + // Act + final beetEntryReturn = BeetApiEntryReturn(plant, beetRow); + // Assert + expect(beetEntryReturn.plant, equals(plant)); + expect(beetEntryReturn.beetRow, equals(beetRow)); + }); +} diff --git a/garden_planner/test/api/api_entities/beet_entry_test.dart b/garden_planner/test/api/api_entities/beet_entry_test.dart new file mode 100644 index 0000000..38e766f --- /dev/null +++ b/garden_planner/test/api/api_entities/beet_entry_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/api/api_entities/beet_entry.dart'; + +void main() { + test('BeetEntry entity Test', () { + // Arrange + const int plantId = 1; + const int position = 2; + const int beetrow = 3; + // Act + final beetEntry = BeetEntry(plantId, position, beetrow); + // Assert + expect(beetEntry.plantId, equals(plantId)); + expect(beetEntry.position, equals(position)); + expect(beetEntry.beetRow, equals(beetrow)); + }); + + test('BeetEntry entity toJson() should work', () { + // Arrange + const int plantId = 1; + const int position = 2; + const int beetrow = 3; + // Act + final beetEntry = BeetEntry(plantId, position, beetrow); + // Act + final jsonMap = beetEntry.toJson(); + + // Assert + expect(jsonMap, isA>()); + expect(jsonMap['plantId'], equals(1)); + expect(jsonMap['position'], equals(2)); + expect(jsonMap['beet_row'], equals(3)); + }); +} diff --git a/garden_planner/test/api/garden_api_test.dart b/garden_planner/test/api/garden_api_test.dart new file mode 100644 index 0000000..383b82b --- /dev/null +++ b/garden_planner/test/api/garden_api_test.dart @@ -0,0 +1,135 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/api/garden_api.service.dart'; +import 'package:garden_planner/entities/beet_entry_return.dart'; + +import '../helpers/plant_generator.dart'; +import '../mock/mock_http_client.dart'; + +void main() { + late GardenApiService gardenApiService; + + setUp(() { + gardenApiService = GardenApiService(MockHttpClient()); + }); + + test('Test getAllPlants', () async { + // Arrange + final expectedPlants = [ + PlantGenerator.getPlant(), + PlantGenerator.getPlant2() + ]; + + // Act + final result = (await gardenApiService.getAllAvailablePlants()).toList(); + + // Assert + for (int i = 0; i < result.length; i++) { + expect(result[i].name, equals(expectedPlants[i].name), + reason: "Name is not equal"); + expect(result[i].supType, equals(expectedPlants[i].supType), + reason: "Description is not equal"); + expect(result[i].verticalSpace, equals(expectedPlants[i].verticalSpace), + reason: "Vertical space is not equal"); + expect( + result[i].horizontalSpace, equals(expectedPlants[i].horizontalSpace), + reason: "Horizontal space is not equal"); + expect(result[i].times.length, equals(expectedPlants[i].times.length), + reason: "Times length is not equal"); + expect(result[i].image, equals(expectedPlants[i].image), + reason: "Image path is not equal"); + expect(result[i].waterRequirement, + equals(expectedPlants[i].waterRequirement), + reason: "Water requirement is not equal"); + + for (int j = 0; j < result[i].times.length; j++) { + expect(result[i].times[j].description, + equals(expectedPlants[i].times[j].description), + reason: "Time description is not equal Plant:$i Time:$j"); + expect(result[i].times[j].action, + equals(expectedPlants[i].times[j].action), + reason: "Time action is not equal Plant:$i Time:$j"); + expect( + result[i].times[j].until, equals(expectedPlants[i].times[j].until), + reason: "Time until is not equal Plant:$i Time:$j"); + expect(result[i].times[j].from, equals(expectedPlants[i].times[j].from), + reason: "Time from is not equal Plant:$i Time:$j"); + } + } + }); + + test('Test loadBeet', () async { + // Arrange + final expectedBeetEntries = [ + BeetApiEntryReturn(PlantGenerator.getPlant2(), 0), + BeetApiEntryReturn(PlantGenerator.getPlant(), 0), + BeetApiEntryReturn(PlantGenerator.getPlant2(), 1), + BeetApiEntryReturn(PlantGenerator.getPlant(), 1) + ]; + // Act + var beetEntries = await gardenApiService.getBeet(); + + // Assert + expect( + beetEntries.length, + equals(expectedBeetEntries.length), + reason: 'Number of beet entries is not equal', + ); + + for (int i = 0; i < beetEntries.length; i++) { + expect( + beetEntries[i].plant.id, + equals(expectedBeetEntries[i].plant.id), + reason: 'plant $i is not equal', + ); + expect( + beetEntries[i].plant.times.length, + equals(expectedBeetEntries[i].plant.times.length), + reason: 'plant.times.length at index $i is not equal', + ); + expect( + beetEntries[i].beetRow, + equals(expectedBeetEntries[i].beetRow), + reason: 'beetrow at index $i is not equal', + ); + } + }); + + test('Test getPlant', () async { + // Arrange + final expectedPlant = PlantGenerator.getPlant(); + + // Act + final plant = await gardenApiService.getPlant(1); + + // Assert + expect(plant.id, equals(expectedPlant.id), reason: 'Plant ID is not equal'); + expect(plant.name, equals(expectedPlant.name), + reason: 'Plant Name is not equal'); + expect(plant.supType, equals(expectedPlant.supType), + reason: 'Plant SubType is not equal'); + expect(plant.waterRequirement, equals(expectedPlant.waterRequirement), + reason: 'Plant Waterrewuirement is not equal'); + expect(plant.horizontalSpace, equals(expectedPlant.horizontalSpace), + reason: 'Plant horizontalSpace is not equal'); + expect(plant.verticalSpace, equals(expectedPlant.verticalSpace), + reason: 'Plant verticalSpace is not equal'); + expect(plant.image, equals(expectedPlant.image), + reason: 'Plant image length is not equal'); + + expect(plant.times.length, equals(expectedPlant.times.length), + reason: 'Planttimes length is not equal'); + for (int j = 0; j < plant.times.length; j++) { + final time = plant.times[j]; + final expectedTime = expectedPlant.times[j]; + + expect(time.description, equals(expectedTime.description), + reason: 'Planttimes description is not equal'); + expect(time.from, equals(expectedTime.from), + reason: 'Planttimes from is not equal'); + expect(time.until, equals(expectedTime.until), + reason: 'Planttimes until is not equal'); + expect(time.action, equals(expectedTime.action), + reason: 'Planttimes action description is not equal'); + } + }); +} diff --git a/garden_planner/test/entities/beet_row_test.dart b/garden_planner/test/entities/beet_row_test.dart new file mode 100644 index 0000000..ce7c945 --- /dev/null +++ b/garden_planner/test/entities/beet_row_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/beet_row.dart'; + +void main() { + test('BeetRow entity Test', () { + // Arrange + const rowId = 1; + // Act + final beetRow = BeetRow(rowId); + // Assert + expect(beetRow.id, equals(rowId), reason: 'id is not equals'); + expect(beetRow.plants, isEmpty, reason: 'plant is not equals'); + }); +} diff --git a/garden_planner/test/entities/beet_test.dart b/garden_planner/test/entities/beet_test.dart new file mode 100644 index 0000000..55dae4f --- /dev/null +++ b/garden_planner/test/entities/beet_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/beet.dart'; + +void main() { + test('Beet entity Add new Row Test', () { + // Arrange + final beet = Beet(); + // Act + beet.addNewRow(); + // Assert + expect(beet.beetRows.length, equals(2), + reason: 'beetrows count should be 1'); + }); + + test('Beet entity Test', () { + // Arrange + final beet = Beet(); + // Assert + //new Beet creats every times the first row + expect(beet.beetRows.length, equals(1), + reason: 'beetrows count should be 1'); + }); +} diff --git a/garden_planner/test/entities/plant_in_row_test.dart b/garden_planner/test/entities/plant_in_row_test.dart new file mode 100644 index 0000000..b36a95d --- /dev/null +++ b/garden_planner/test/entities/plant_in_row_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/entities/plant_in_row.dart'; + +void main() { + test('PlantInRow entity Test', () { + // Arrange + const int plantId = 1; + const String name = 'Plant 1'; + const double waterRequirement = 2; + const String description = "test Descriptiom"; + const double horizontalSpace = 3; + const double verticalSpace = 4; + const String image = "test image"; + + final plant = Plant( + name: name, + id: plantId, + waterRequirement: waterRequirement, + supType: description, + horizontalSpace: horizontalSpace, + verticalSpace: verticalSpace); + + plant.image = image; + + // Act + final plantInRow = PlantInRow(position: 0, plant: plant); + + // Assert + expect(plantInRow.position, equals(0), reason: 'position should be 0'); + expect(plantInRow.id, equals(plantId), reason: 'id should be $plantId'); + expect(plantInRow.name, equals(name), reason: 'name should be $name'); + expect(plantInRow.supType, equals(description), + reason: 'SupType should be $description'); + expect(plantInRow.verticalSpace, equals(verticalSpace), + reason: 'Verical Space should be $verticalSpace'); + expect(plantInRow.horizontalSpace, equals(horizontalSpace), + reason: 'Horizontal Space should be $horizontalSpace'); + expect(plantInRow.waterRequirement, equals(waterRequirement), + reason: 'Water requirement should be $waterRequirement'); + expect(plantInRow.image, equals(image), reason: 'image should be $image'); + }); +} diff --git a/garden_planner/test/entities/plant_test.dart b/garden_planner/test/entities/plant_test.dart new file mode 100644 index 0000000..8ec8d7f --- /dev/null +++ b/garden_planner/test/entities/plant_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/plant.dart'; + +void main() { + test('Plant entity Test', () { + // Arrange + const int plantId = 1; + const String name = 'Plant 1'; + const double waterRequirement = 2; + const String description = "test Description"; + const double horizontalSpace = 3; + const double verticalSpace = 4; + const String image = "test image"; + + //Act + final plant = Plant( + name: name, + id: plantId, + waterRequirement: waterRequirement, + supType: description, + horizontalSpace: horizontalSpace, + verticalSpace: verticalSpace); + + plant.image = image; + + // Assert + expect(plant.id, equals(plantId), reason: 'plantId should be $plantId'); + expect(plant.name, equals(name), reason: 'name should be $name'); + expect(plant.supType, equals(description), + reason: 'SupType should be $description'); + expect(plant.verticalSpace, equals(verticalSpace), + reason: 'Vertical Space should be $verticalSpace'); + expect(plant.horizontalSpace, equals(horizontalSpace), + reason: 'Horizontal Space should be $horizontalSpace'); + expect(plant.waterRequirement, equals(waterRequirement), + reason: 'Water Requirement should be $waterRequirement'); + expect(plant.image, equals(image), reason: 'Image should be $image'); + }); +} diff --git a/garden_planner/test/entities/plant_time_test.dart b/garden_planner/test/entities/plant_time_test.dart new file mode 100644 index 0000000..85e61bd --- /dev/null +++ b/garden_planner/test/entities/plant_time_test.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/plant_time.dart'; + +void main() { + test('PlantTime entity test', () { + // Arrange + final DateTime from = DateTime(2023, 6, 1); + final DateTime until = DateTime(2023, 6, 30); + const String description = 'test Beschreibung'; + const Color color = Color(0xFF00FF00); + const bool action = true; + + // Act + final plantTime = PlantTime( + from: from, + until: until, + description: description, + color: color, + action: action, + ); + + // Assert + expect(plantTime.from, equals(from), reason: 'from should be $from'); + expect(plantTime.until, equals(until), reason: 'until should be $until'); + expect(plantTime.description, equals(description), + reason: 'Description should be $description'); + expect(plantTime.color, equals(color), reason: 'Color should be $color'); + expect(plantTime.action, equals(action), + reason: 'Action should be $action'); + }); +} diff --git a/garden_planner/test/helpers/beet_repository_generator.dart b/garden_planner/test/helpers/beet_repository_generator.dart new file mode 100644 index 0000000..98adfa2 --- /dev/null +++ b/garden_planner/test/helpers/beet_repository_generator.dart @@ -0,0 +1,14 @@ +import 'package:garden_planner/api/garden_api.service.dart'; +import 'package:garden_planner/logic/beet.service.dart'; +import 'package:garden_planner/logic/beet_row.service.dart'; +import 'package:garden_planner/logic/plant.service.dart'; +import 'package:garden_planner/repositories/beet.repositories.dart'; + +import '../mock/mock_http_client.dart'; + +class BeetRepositoryGenerator { + static getBeetRepository() { + return BeetRepository(BeetRowService(), PlantService(), + GardenApiService(MockHttpClient()), BeetService()); + } +} diff --git a/garden_planner/test/helpers/plant_generator.dart b/garden_planner/test/helpers/plant_generator.dart new file mode 100644 index 0000000..df96408 --- /dev/null +++ b/garden_planner/test/helpers/plant_generator.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/entities/plant_time.dart'; + +class PlantGenerator { + static getPlant() { + return Plant( + id: 1, + name: 'Tomate', + supType: '', + waterRequirement: 0.8, + horizontalSpace: 0.6, + verticalSpace: 0.8, + imagePath: 'lib/assets/plants/tomatoes-gc17bf34c6_640.jpg', + times: [ + PlantTime( + color: Colors.yellow, + description: 'Aussaat', + from: DateTime.parse('2023-04-01T00:00:00.000Z'), + until: DateTime.parse('2023-05-15T00:00:00.000Z'), + action: true, + ), + PlantTime( + color: Colors.purple, + description: 'Wachstumsphase', + from: DateTime.parse('2023-05-16T00:00:00.000Z'), + until: DateTime.parse('2023-06-15T00:00:00.000Z'), + action: false, + ), + PlantTime( + color: Colors.red, + description: 'Erntezeit', + from: DateTime.parse('2023-06-16T00:00:00.000Z'), + until: DateTime.parse('2023-07-31T00:00:00.000Z'), + action: false, + ), + ], + ); + } + + static getPlant2() { + Plant plant = Plant( + id: 2, + name: 'Kopfsalat', + supType: 'Maikönig', + waterRequirement: 0.5, + horizontalSpace: 0.25, + verticalSpace: 0.25, + imagePath: 'lib/assets/plants/salad-seedling-g46a52dd37_640.jpg', + times: [ + PlantTime( + color: Colors.yellow, + description: 'Aussaat', + from: DateTime.parse('2023-04-01T00:00:00.000Z'), + until: DateTime.parse('2023-06-01T00:00:00.000Z'), + action: true, + ), + PlantTime( + color: Colors.purple, + description: 'Wachstumsphase', + from: DateTime.parse('2023-06-02T00:00:00.000Z'), + until: DateTime.parse('2023-07-15T00:00:00.000Z'), + action: false, + ), + PlantTime( + color: Colors.red, + description: 'Erntezeit', + from: DateTime.parse('2023-07-16T00:00:00.000Z'), + until: DateTime.parse('2023-08-31T00:00:00.000Z'), + action: false, + ) + ]); + + return plant; + } +} diff --git a/garden_planner/test/logic/beet.service_test.dart b/garden_planner/test/logic/beet.service_test.dart new file mode 100644 index 0000000..e19026a --- /dev/null +++ b/garden_planner/test/logic/beet.service_test.dart @@ -0,0 +1,156 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/beet.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/logic/beet.service.dart'; +import 'package:garden_planner/logic/beet_row.service.dart'; +import 'package:garden_planner/logic/plant.service.dart'; + +import '../helpers/plant_generator.dart'; + +void main() { + test('BeetService Add Row should increase the Rows', () { + // Arrange + final Beet beet = Beet(); + final BeetService beetService = BeetService(); + // Act + beetService.addNewRow(beet); + + // Assert + // expect(beet.beetrows, equals(2)); + }); + + test('BeetService addPlantToRowById should work', () { + // Arrange + final BeetService beetService = BeetService(); + final Beet beet = Beet(); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + + // Act + beetService.addPlantToRowById(beet, beetRowService, 0, plant); + + beetService.addPlantToRowById(beet, beetRowService, 0, plant2); + + // row existiert nicht + expect(() => beetService.addPlantToRowById(beet, beetRowService, 1, plant), + throwsStateError); + + // Assert + expect(beet.beetRows.length, equals(1), reason: 'It should be ine row'); + expect(beet.beetRows[0].plants.length, equals(2), + reason: 'it should be plants two rows in the row'); + expect(beet.beetRows[0].plants[0].id, equals(plant.id), + reason: 'Id is not right for plant1'); + expect(beet.beetRows[0].plants[1].id, equals(plant2.id), + reason: 'Id is not right for plant 2'); + }); + + test('BeetService addNewRow should work', () { + // Arrange + final BeetService beetService = BeetService(); + final Beet beet = Beet(); + + // Act + beetService.addNewRow(beet); + beetService.addNewRow(beet); + + // Assert + expect(beet.beetRows.length, equals(3), reason: 'it should be 3 rows'); + expect(beet.beetRows[0].id, equals(0), reason: 'Row 1 id should be 0'); + expect(beet.beetRows[1].id, equals(1), reason: 'Row 2 id should be 1'); + expect(beet.beetRows[2].id, equals(2), reason: 'Row 3 id should be 2'); + }); + + test('BeetService getMaxHorizontalSpace should work', () { + // Arrange + final BeetService beetService = BeetService(); + final Beet beet = Beet(); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + + beetService.addNewRow(beet); + + beetService.addPlantToRowById(beet, beetRowService, 0, plant); + + beetService.addPlantToRowById(beet, beetRowService, 0, plant); + + beetService.addPlantToRowById(beet, beetRowService, 1, plant); + + beetService.addPlantToRowById(beet, beetRowService, 1, plant2); + + // Act + var getMaxHorizontalSpace = + beetService.getMaxHorizontalSpaceOfRows(beetRowService, beet); + + // Assert + // row 1 = 0,6 + 0,6 + // row 2 = 0.25 + 0.25 + expect(getMaxHorizontalSpace, equals(1.2), + reason: 'Horizontal space should be 1.2'); + }); + + test('BeetService isActionNeeded should work', () { + // Arrange + final BeetService beetService = BeetService(); + final Beet beet = Beet(); + final BeetRowService beetRowService = BeetRowService(); + final PlantService plantService = PlantService(); + + final Plant plant = PlantGenerator.getPlant(); + + beetService.addPlantToRowById(beet, beetRowService, 0, plant); + + // Act + var actionShouldBeTrue = beetService.isActionNeeded( + beetRowService, plantService, beet, DateTime(2023, 05, 01)); + + var actionShouldBeFalseBecauseNoEntry = beetService.isActionNeeded( + beetRowService, plantService, beet, DateTime(2023, 07, 01)); + var actionShouldBeFalse = beetService.isActionNeeded( + beetRowService, plantService, beet, DateTime(2023, 10, 01)); + + // Assert + expect(actionShouldBeTrue, equals(true), reason: 'Action should be true'); + expect(actionShouldBeFalse, equals(false), + reason: 'action should be false because no entry'); + expect(actionShouldBeFalseBecauseNoEntry, equals(false), + reason: 'Action should be false'); + }); + + test('BeetService getVerticalSpaceValue should work', () { + // Arrange + final BeetService beetService = BeetService(); + final Beet beet = Beet(); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + + beetService.addNewRow(beet); + + beetService.addPlantToRowById(beet, beetRowService, 0, plant); + + beetService.addPlantToRowById(beet, beetRowService, 0, plant); + + beetService.addPlantToRowById(beet, beetRowService, 1, plant); + + beetService.addPlantToRowById(beet, beetRowService, 1, plant2); + + // Act + var getVerticalSpaceValue = + beetService.getVerticalPlantingSpace(beetRowService, beet); + + // Assert + // Every Row takes the half Vertical Space, because the Plant will be placed in the middle + // for the second row, its the hole space of the previews and the half of the actual row + // in both rows is the biggest space 0.8 + var row1 = (0.8 / 2); + var row2 = (0.8 + (0.8 / 2)); + + expect(getVerticalSpaceValue, equals([row1, row2]), + reason: 'Vertical space is not right'); + }); + + // DB Tests will be in next Release +} diff --git a/garden_planner/test/logic/beet_row.service_test.dart b/garden_planner/test/logic/beet_row.service_test.dart new file mode 100644 index 0000000..1cee583 --- /dev/null +++ b/garden_planner/test/logic/beet_row.service_test.dart @@ -0,0 +1,183 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/beet.dart'; +import 'package:garden_planner/entities/beet_row.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/entities/plant_in_row.dart'; +import 'package:garden_planner/logic/beet.service.dart'; +import 'package:garden_planner/logic/beet_row.service.dart'; +import 'package:garden_planner/logic/plant.service.dart'; + +import '../helpers/plant_generator.dart'; + +void main() { + test('BeetRowService gethorizontalSpace should work', () { + //Plant1 Horizontal 3 + //Plant2 Horizontal 6 + + // Arrange + final BeetRow row = BeetRow(0); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + + beetRowService.addPlant(row, plant); + beetRowService.addPlant(row, plant2); + + // Act + var getMaxHorizontalSpace = beetRowService.getTotalHorizontalSpace(row); + + // Assert + // space = 0.6+0.25 + expect(getMaxHorizontalSpace, equals(0.85), reason: ' Space should be 0.8'); + }); + + test('BeetRowService getHorizontalSpaceValue should work', () { + //Plant1 Horizontal 3 + //Plant2 Horizontal 6 + + // Arrange + final BeetRow row = BeetRow(0); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + + beetRowService.addPlant(row, plant); + beetRowService.addPlant(row, plant2); + + // Act + var getMaxHorizontalSpaceValues = + beetRowService.getHorizontalPlantingPosition(row); + + // Assert + // Every Row takes the half horizontal Space, because the Plant will be placed in the middle + // for the second row, its the hole space of the previews and the half of the actual row + var plantHorizontal1 = (0.6 / 2); + var plantHorizontal2 = 0.6 + (0.25 / 2); + + expect(getMaxHorizontalSpaceValues, + equals([plantHorizontal1, plantHorizontal2]), + reason: 'value is not right '); + }); + + test('BeetRowService isActionNeeded should work', () { + // Arrange + final BeetRow row = BeetRow(0); + + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + final PlantService plantService = PlantService(); + + beetRowService.addPlant(row, plant); + beetRowService.addPlant(row, plant2); + + // Act + var actionShouldBeTrue = beetRowService.isActionNeeded( + plantService, row, DateTime(2023, 05, 01)); + + var actionShouldBeFalseBecauseNoEntry = beetRowService.isActionNeeded( + plantService, row, DateTime(2023, 07, 01)); + var actionShouldBeFalse = beetRowService.isActionNeeded( + plantService, row, DateTime(2023, 10, 01)); + + // Assert + expect(actionShouldBeTrue, equals(true), reason: 'action should be true '); + expect(actionShouldBeFalse, equals(false), + reason: 'action should be false because no entry '); + expect(actionShouldBeFalseBecauseNoEntry, equals(false), + reason: 'action should be false because its outside the date'); + }); + + test('BeetRowService addPlant should work', () { + // Arrange + final BeetRow row = BeetRow(0); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + + // Act + beetRowService.addPlant(row, plant); + beetRowService.addPlant(row, plant2); + + // Assert + expect(row.id, equals(0), reason: 'RowId is not 0'); + expect(row.plants.length, equals(2), reason: 'Not all plants add'); + + expect(row.plants[0], isA(), + reason: 'plant1 not the right type'); + expect(row.plants[1], isA(), + reason: 'plant2 not the right type'); + + expect(row.plants[0].id, equals(plant.id), + reason: 'plant1 is not the right plant'); + expect(row.plants[1].id, equals(plant2.id), + reason: 'plant2 is not the right plant'); + }); + + test('BeetRowService removePlantFromRowById should work', () { + // Arrange + final BeetRow row = BeetRow(0); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + + beetRowService.addPlant(row, plant); + beetRowService.addPlant(row, plant2); + beetRowService.addPlant(row, plant); + + // Act + + beetRowService.removePlantFromRowById(row, 1); + + // Assert + expect(row.plants.length, equals(2), reason: 'Row was not deleted'); + expect(row.plants[0].id, equals(plant.id), + reason: 'Row 1 is not the right'); + expect(row.plants[1].id, equals(plant.id), + reason: 'Row 1 is not the right'); + }); + + test('BeetRowService removePlantFromRowById should throw no Exception', () { + // Arrange + final BeetRow row = BeetRow(0); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant = PlantGenerator.getPlant(); + + beetRowService.addPlant(row, plant); + + // Act + + beetRowService.removePlantFromRowById(row, 14); + + // Assert + // No exception should be thrown + }); + + test('BeetRowService removePlantFromRowById should work', () { + // Arrange + final BeetService beetService = BeetService(); + final Beet beet = Beet(); + final BeetRowService beetRowService = BeetRowService(); + final Plant plant1 = PlantGenerator.getPlant(); + final Plant plant2 = PlantGenerator.getPlant2(); + + beetService.addNewRow(beet); + beetService.addNewRow(beet); + + beetService.addPlantToRowById(beet, beetRowService, 0, plant1); + + beetService.addPlantToRowById(beet, beetRowService, 1, plant1); + + beetService.addPlantToRowById(beet, beetRowService, 1, plant2); + // Act + + var row = beetRowService.getRowById(beet, 1); + + // Assert + expect(row.plants.length, equals(2), reason: 'Is the wrong row returned'); + expect(row.plants[1].id, equals(plant2.id), + reason: 'Is the wrong row returned'); + }); + + // DB Tests will be in next Release +} diff --git a/garden_planner/test/logic/date.helper_test.dart b/garden_planner/test/logic/date.helper_test.dart new file mode 100644 index 0000000..be2e94c --- /dev/null +++ b/garden_planner/test/logic/date.helper_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/logic/date.helper.dart'; + +void main() { + test('DateHelper isDateBetween should work in the same year', () { + final dateBetween = DateTime(2023, 6, 15); + final dateOutside = DateTime(2023, 7, 15); + final from = DateTime(2023, 6, 1); + final until = DateTime(2023, 6, 30); + + final resultTrue = DateHelper.isDateBetween(dateBetween, from, until); + final resultFalse = DateHelper.isDateBetween(dateOutside, from, until); + + expect(resultTrue, true, reason: 'Date is beetween but returns true'); + expect(resultFalse, false, reason: 'Date is outside but returns false'); + }); + + test('DateHelper isDateBetween should work with differnet year', () { + final dateBetween = DateTime(2020, 6, 15); + final dateOutside = DateTime(2021, 7, 15); + final from = DateTime(2022, 6, 1); + final until = DateTime(2023, 6, 30); + + final resultTrue = DateHelper.isDateBetween(dateBetween, from, until); + final resultFalse = DateHelper.isDateBetween(dateOutside, from, until); + + expect(resultTrue, true, reason: 'Date is beetween but returns true'); + expect(resultFalse, false, reason: 'Date is outside but returns false'); + }); + + test('DateHelper isDateBetween should work with extremValues', () { + final dateOutside1 = DateTime(2020, 6, 1); + final dateOutside2 = DateTime(2021, 6, 31); + final dateBetween1 = DateTime(2020, 6, 2); + final dateBetween2 = DateTime(2021, 6, 30); + final from = DateTime(2022, 6, 2); + final until = DateTime(2023, 6, 30); + + final resultFalse1 = DateHelper.isDateBetween(dateOutside1, from, until); + final resultFalse2 = DateHelper.isDateBetween(dateOutside2, from, until); + final resultTrue1 = DateHelper.isDateBetween(dateBetween1, from, until); + final resultTrue2 = DateHelper.isDateBetween(dateBetween2, from, until); + + expect(resultTrue1, true, + reason: 'dateBetween1 is between but returns false'); + expect(resultTrue2, true, + reason: 'dateBetween2 is between but returns false'); + expect(resultFalse1, false, + reason: 'dateOutside1 is outside but returns true'); + expect(resultFalse2, false, + reason: 'dateOutside2 is outside but returns true'); + }); + + test('getDateTimeByDayOfYear returns the correct date', () { + const dayOfYear = 40; + final expectedDate = DateTime(2023, 2, 9); + + final result = DateHelper.getDateTimeByDayOfYear(dayOfYear); + + expect(result, expectedDate, + reason: "The 40 day should be the 9th february"); + }); + + test('getDayOfYear returns the correct day of the year', () { + final date = DateTime(2000, 2, 9); + const expectedDayOfYear = 40; + + final result = DateHelper.getDayOfYear(date); + + expect(result, expectedDayOfYear, + reason: "The 9th february should be the 40 day"); + }); + + test('getDateTimeByDate returns the correct date', () { + final date = DateTime(2020, 6, 15); + final expectedDate = DateTime(2023, 6, 15); + + final result = DateHelper.transformToCurrentYear(date); + + expect(result, expectedDate); + }); +} diff --git a/garden_planner/test/logic/plant.service_test.dart b/garden_planner/test/logic/plant.service_test.dart new file mode 100644 index 0000000..36c07d6 --- /dev/null +++ b/garden_planner/test/logic/plant.service_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/constance.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/logic/plant.service.dart'; + +import '../helpers/plant_generator.dart'; + +void main() { + test('PlantService isActionNeeded should work', () { + // Arrange + final PlantService plantService = PlantService(); + final Plant plant = PlantGenerator.getPlant(); + + // Act + var actionShouldBeTrue = + plantService.isActionNeeded(plant, DateTime(2023, 05, 01)); + var actionShouldBeFalseBecauseNoEntry = + plantService.isActionNeeded(plant, DateTime(2023, 07, 01)); + var actionShouldBeFalse = + plantService.isActionNeeded(plant, DateTime(2023, 10, 01)); + + // Assert + expect(actionShouldBeTrue, equals(true), + reason: 'PlantService isActionNeeded ist between but returns false'); + expect(actionShouldBeFalse, equals(false), + reason: 'PlantService isActionNeeded ist outside set but returns true'); + expect(actionShouldBeFalseBecauseNoEntry, equals(false), + reason: 'PlantService isActionNeeded ist not set but returns true'); + }); + + test('PlantService getTimeDescription should work', () { + // Arrange + final PlantService plantService = PlantService(); + final Plant plant = PlantGenerator.getPlant(); + + // Act + var descritpionJanuar = + plantService.getTimeDescription(plant, DateTime(2023, 01, 01)); + var descritpionApril = + plantService.getTimeDescription(plant, DateTime(2023, 04, 02)); + var descritpionMai = + plantService.getTimeDescription(plant, DateTime(2023, 05, 20)); + var descritpionJuni = + plantService.getTimeDescription(plant, DateTime(2023, 06, 20)); + + // Assert + expect(descritpionJanuar, equals(Constance.defaultDescription), + reason: 'Description is not equal'); + expect(descritpionMai, equals('Wachstumsphase'), + reason: 'Description is not equal'); + expect(descritpionApril, equals('Aussaat'), + reason: 'Description is not equal'); + expect(descritpionJuni, equals('Erntezeit'), + reason: 'Description is not equal'); + }); + + test('PlantService getTimeDescription extremValues should work', () { + // Arrange + final PlantService plantService = PlantService(); + final Plant plant = PlantGenerator.getPlant(); + // Act + var descritpionBevorWachstum = + plantService.getTimeDescription(plant, DateTime(2023, 05, 15)); + var descritpionWachstumstart = + plantService.getTimeDescription(plant, DateTime(2023, 05, 16)); + var descritpionWachstumEnd = + plantService.getTimeDescription(plant, DateTime(2023, 06, 15)); + var descritpionAfterWachstum = + plantService.getTimeDescription(plant, DateTime(2023, 06, 16)); + + // Assert + expect(descritpionBevorWachstum, isNot(equals('Wachstumsphase')), + reason: 'Description returns wrong value bevor'); + expect(descritpionWachstumstart, equals('Wachstumsphase'), + reason: 'Description returns wrong value start'); + expect(descritpionWachstumEnd, equals('Wachstumsphase'), + reason: 'Description returns wrong value end'); + expect(descritpionAfterWachstum, isNot(equals('Wachstumsphase')), + reason: 'Description returns wrong value after'); + }); + + test('PlantService getColor should work', () { + // Arrange + final PlantService plantService = PlantService(); + final Plant plant = PlantGenerator.getPlant(); + + // Act + var descritpionJanuar = + plantService.getColor(plant, DateTime(2023, 01, 01)); + var descritpionApril = plantService.getColor(plant, DateTime(2023, 04, 02)); + var descritpionMai = plantService.getColor(plant, DateTime(2023, 05, 20)); + var descritpionJuni = plantService.getColor(plant, DateTime(2023, 06, 20)); + + // Assert + expect(descritpionJanuar, equals(Constance.defaultColor), + reason: 'Color is not equal'); + expect(descritpionMai, equals(Colors.purple), + reason: 'Description is not equal'); + expect(descritpionApril, equals(Colors.yellow), + reason: 'Description is not equal'); + expect(descritpionJuni, equals(Colors.red), + reason: 'Description is not equal'); + }); +} diff --git a/garden_planner/test/mock/mock_http_client.dart b/garden_planner/test/mock/mock_http_client.dart new file mode 100644 index 0000000..3ca166d --- /dev/null +++ b/garden_planner/test/mock/mock_http_client.dart @@ -0,0 +1,190 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/api/http_connection.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/mockito.dart'; + +class MockHttpClient extends Mock implements HttpConnector { + @override + + Future getBeet() async { + final mockResponse = http.Response('[' + '{"id":11,"plant_id":2,"position":0,"beet_row":0},' + '{"id":12,"plant_id":1,"position":1,"beet_row":0},' + '{"id":13,"plant_id":2,"position":0,"beet_row":1},' + '{"id":14,"plant_id":1,"position":1,"beet_row":1}]', 200); + + return mockResponse; + } + + + @override + Future getAllPlants() async { + final mockResponse = http.Response('''[ + { + "id": 1, + "name": "Tomate", + "description": "", + "water_requirement": 0.8, + "horizontal_space": 0.6, + "vertical_space": 0.8, + "image_path": "lib/assets/plants/tomatoes-gc17bf34c6_640.jpg", + "times": [ + { + "id": 1, + "plant_id": 1, + "color": "4294961979", + "description": "Aussaat", + "from_date": "2023-04-01T00:00:00.000Z", + "until_date": "2023-05-15T00:00:00.000Z", + "action_needed": true + }, + { + "id": 2, + "plant_id": 1, + "color": "438858537", + "description": "Wachstumsphase", + "from_date": "2023-05-16T00:00:00.000Z", + "until_date": "2023-06-15T00:00:00.000Z", + "action_needed": false + }, + { + "id": 3, + "plant_id": 1, + "color": "4294198070", + "description": "Erntezeit", + "from_date": "2023-06-16T00:00:00.000Z", + "until_date": "2023-07-31T00:00:00.000Z", + "action_needed": false + } + ] + }, + { + "id": 2, + "name": "Kopfsalat", + "description": "Maikönig", + "water_requirement": 0.5, + "horizontal_space": 0.25, + "vertical_space": 0.25, + "image_path": "lib/assets/plants/salad-seedling-g46a52dd37_640.jpg", + "times": [ + { + "id": 4, + "plant_id": 2, + "color": "4294961979", + "description": "Aussaat", + "from_date": "2023-04-01T00:00:00.000Z", + "until_date": "2023-06-01T00:00:00.000Z", + "action_needed": true + }, + { + "id": 5, + "plant_id": 2, + "color": "438858537", + "description": "Wachstumsphase", + "from_date": "2023-06-02T00:00:00.000Z", + "until_date": "2023-07-15T00:00:00.000Z", + "action_needed": false + }, + { + "id": 6, + "plant_id": 2, + "color": "4294198070", + "description": "Erntezeit", + "from_date": "2023-07-16T00:00:00.000Z", + "until_date": "2023-08-31T00:00:00.000Z", + "action_needed": false + } + ] + } + ]''', 200); + + return mockResponse; + } + + @override + Future getPlant(int id) async { + if (id == 1) { + return http.Response('''{ + "id": 1, + "name": "Tomate", + "description": "", + "water_requirement": 0.8, + "horizontal_space": 0.6, + "vertical_space": 0.8, + "image_path": "lib/assets/plants/tomatoes-gc17bf34c6_640.jpg", + "times": [ + { + "id": 1, + "plant_id": 1, + "color": "4294961979", + "description": "Aussaat", + "from_date": "2023-04-01T00:00:00.000Z", + "until_date": "2023-05-15T00:00:00.000Z", + "action_needed": true + }, + { + "id": 2, + "plant_id": 1, + "color": "438858537", + "description": "Wachstumsphase", + "from_date": "2023-05-16T00:00:00.000Z", + "until_date": "2023-06-15T00:00:00.000Z", + "action_needed": false + }, + { + "id": 3, + "plant_id": 1, + "color": "4294198070", + "description": "Erntezeit", + "from_date": "2023-06-16T00:00:00.000Z", + "until_date": "2023-07-31T00:00:00.000Z", + "action_needed": false + } + ] + }''', 200); + } + + if (id == 2) { + return http.Response(''' + { + "id": 2, + "name": "Kopfsalat", + "description": "Maikönig", + "water_requirement": 0.5, + "horizontal_space": 0.25, + "vertical_space": 0.25, + "image_path": "lib/assets/plants/salad-seedling-g46a52dd37_640.jpg", + "times": [ + { + "id": 4, + "plant_id": 2, + "color": "4294961979", + "description": "Aussaat", + "from_date": "2023-04-01T00:00:00.000Z", + "until_date": "2023-06-01T00:00:00.000Z", + "action_needed": true + }, + { + "id": 5, + "plant_id": 2, + "color": "438858537", + "description": "Wachstumsphase", + "from_date": "2023-06-02T00:00:00.000Z", + "until_date": "2023-07-15T00:00:00.000Z", + "action_needed": false + }, + { + "id": 6, + "plant_id": 2, + "color": "4294198070", + "description": "Erntezeit", + "from_date": "2023-07-16T00:00:00.000Z", + "until_date": "2023-08-31T00:00:00.000Z", + "action_needed": false + } + ] + }''', 200); + } + return http.Response('', 404); + } +} diff --git a/garden_planner/test/test.dart b/garden_planner/test/test.dart new file mode 100644 index 0000000..9fff88f --- /dev/null +++ b/garden_planner/test/test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/main.dart'; + +import 'helpers/beet_repository_generator.dart'; + +void main() { + testWidgets('GardenPlanner Test', (WidgetTester tester) async { + + await tester.pumpWidget( + GardenPlanner( + beetRepository: BeetRepositoryGenerator.getBeetRepository() + ) + ); + + }); +} diff --git a/garden_planner/test/test.sh b/garden_planner/test/test.sh new file mode 100644 index 0000000..781e298 --- /dev/null +++ b/garden_planner/test/test.sh @@ -0,0 +1,5 @@ +cd .. + +flutter test --coverage +genhtml coverage/lcov.info -o coverage/html +open coverage/html/index.html diff --git a/garden_planner/test/widgets/content_test.dart b/garden_planner/test/widgets/content_test.dart new file mode 100644 index 0000000..6658a79 --- /dev/null +++ b/garden_planner/test/widgets/content_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/constance.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/entities/plant_in_row.dart'; +import 'package:garden_planner/repositories/beet.repositories.dart'; +import 'package:garden_planner/widgets/content.dart'; +import 'package:garden_planner/widgets/content_widgets/control_bar.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard.dart'; +import 'package:garden_planner/widgets/content_widgets/footer.dart'; +import 'package:garden_planner/widgets/content_widgets/new_beet_row.dart'; + +import '../helpers/beet_repository_generator.dart'; +import '../helpers/plant_generator.dart'; + +void main() { + testWidgets('Content widget displays all widgets', + (WidgetTester tester) async { + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Content( + beetRepository: BeetRepositoryGenerator.getBeetRepository(), + ), + ), + ), + ); + + //Assert + expect(find.byType(Control), findsOneWidget, + reason: 'Control widget is missing'); + expect(find.byType(Dashboard), findsOneWidget, + reason: 'Control widget is missing'); + expect(find.byType(NewBeetRow), findsOneWidget, + reason: 'Control widget is missing'); + expect(find.byType(Footer), findsOneWidget, + reason: 'Control widget is missing'); + }); + + testWidgets('Content widget drop Plant delegate work', + (WidgetTester tester) async { + + final Plant plant = PlantGenerator.getPlant(); + final BeetRepository beetrepo = BeetRepositoryGenerator.getBeetRepository(); + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Content( + beetRepository: beetrepo, + ), + ), + ), + ); + + //Act + final contentState = tester.state(find.byType(Content)); + contentState.plantDroppedOnRow(0, plant); + + // Assert Plant is Dropped + expect(beetrepo.getRow(0).plants.length, 1,reason: 'No Plant saved'); + expect(beetrepo.getRow(0).plants[0].id, plant.id,reason: 'Plant drop should be same Plant'); + }); + + testWidgets('Content widget new Row', (WidgetTester tester) async { + final BeetRepository beetrepo = BeetRepositoryGenerator.getBeetRepository(); + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Content( + beetRepository: beetrepo, + ), + ), + ), + ); + + //Arrange + for (int i = 1; i < Constance.maxNumberOfRows; i++) { + await tester.tap(find.text('Neue Reihe')); + await tester.pumpAndSettle(); + } + // Assert Plant is Dropped + expect(find.byType(NewBeetRow), findsNothing, + reason: 'No Button should appear after max Numbers of row'); + expect(beetrepo.beet.beetRows.length, equals(Constance.maxNumberOfRows), + reason: 'Rows should be max'); + }); + + testWidgets('Content widget drop', (WidgetTester tester) async { + final BeetRepository beetrepo = BeetRepositoryGenerator.getBeetRepository(); + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Content( + beetRepository: beetrepo, + ), + ), + ), + ); + + //Act + final contentState = tester.state(find.byType(Content)); + final newDate = DateTime(2023, 6, 1); + contentState.newDaySelected(newDate); + + // Assert Date is changed + expect(contentState.selectedDate, newDate, reason: 'Date not saved'); + }); + + testWidgets('Content widget reomove plant', (WidgetTester tester) async { + final BeetRepository beetrepo = BeetRepositoryGenerator.getBeetRepository(); + + //Arrange + beetrepo.addPlantToRowById(0, PlantGenerator.getPlant()); + beetrepo.addPlantToRowById(0, PlantGenerator.getPlant2()); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Content( + beetRepository: beetrepo, + ), + ), + ), + ); + + //Act + final contentState = tester.state(find.byType(Content)); + contentState.plantRemovedFromRow( + 0, PlantInRow(position: 1, plant: PlantGenerator.getPlant())); + + // Assert Date is changed + expect(beetrepo.getRow(0).plants.length, 1, + reason: 'plants should be removed'); + }); +} diff --git a/garden_planner/test/widgets/content_widgets/control_bar_test.dart b/garden_planner/test/widgets/content_widgets/control_bar_test.dart new file mode 100644 index 0000000..962b7ff --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/control_bar_test.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/widgets/content_widgets/control_bar.dart'; + +void main() { + testWidgets('Control widget checkboxes working', (WidgetTester tester) async { + bool isShowSpaceChanged = false; + bool isImagesChanged = false; + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Control( + showSpaceRequirements: false, + onShowSpaceChanged: (value) => isShowSpaceChanged = true, + showImages: false, + onImagesChanged: (value) => isImagesChanged = true, + actionIsNeeded: false, + ), + ), + ), + ); + + expect(find.byType(Checkbox), findsNWidgets(2)); + + // Act + await tester.tap(find.byType(Checkbox).at(0)); + await tester.pump(); + + await tester.tap(find.byType(Checkbox).at(1)); + await tester.pump(); + + // Assert + expect(isShowSpaceChanged, true, reason: 'Space value not changed'); + expect(isImagesChanged, true, reason: 'Space value not changed'); + }); + + testWidgets('Control widget Action needed false', + (WidgetTester tester) async { + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Control( + showSpaceRequirements: false, + onShowSpaceChanged: (value) => {}, + showImages: false, + onImagesChanged: (value) => {}, + actionIsNeeded: false, + ), + ), + ), + ); + + // Assert + expect(find.byIcon(Icons.warning), findsNothing, + reason: 'sholud not be shown "Icon"'); + expect(find.text("Aktion nötig"), findsNothing, + reason: 'sholud not be shown "Aktion nötig"'); + expect(find.text("Nichts zu tun"), findsOneWidget, + reason: '"Nichts zu tun" should be displayed'); + }); + + testWidgets('Control widget Action needed true >300', + (WidgetTester tester) async { + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 800, + child: Control( + showSpaceRequirements: false, + onShowSpaceChanged: (value) => {}, + showImages: false, + onImagesChanged: (value) => {}, + actionIsNeeded: true, + ), + )), + ), + ); + + // Assert + expect(find.byIcon(Icons.warning), findsOneWidget, + reason: '"Icons." should be displayed'); + expect(find.text('Aktion nötig'), findsOneWidget, + reason: '"Aktion nötig" should be display'); + expect(find.text('Nichts zu tun'), findsNothing, + reason: 'sholud not be shown "Nichts zu tun"'); + }); + + testWidgets('Control widget Action needed true <300', + (WidgetTester tester) async { + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 290, + child: Control( + showSpaceRequirements: false, + onShowSpaceChanged: (value) => {}, + showImages: false, + onImagesChanged: (value) => {}, + actionIsNeeded: true, + ), + )), + ), + ); + + // Assert + expect(find.byIcon(Icons.warning), findsOneWidget, + reason: 'sholud not be shown because size is under 300'); + expect(find.text('' "Aktion nötig"), findsNothing, + reason: 'sholud not be shown because size is under 300'); + expect(find.text('Nichts zu tun'), findsNothing, + reason: 'sholud not be shown "Nichts zu tun"'); + }); +} diff --git a/garden_planner/test/widgets/content_widgets/dashboard_test.dart b/garden_planner/test/widgets/content_widgets/dashboard_test.dart new file mode 100644 index 0000000..07ed31f --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/dashboard_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/repositories/beet.repositories.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/plant_row.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/space/plant_row_horizontal_space.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/space/plant_row_space.dart'; + +import '../../helpers/beet_repository_generator.dart'; +import '../../helpers/plant_generator.dart'; + +void main() { + late BeetRepository beetRepository; + + setUp(() { + beetRepository = BeetRepositoryGenerator.getBeetRepository(); + + beetRepository.addNewRowToBeet(); + beetRepository.addNewRowToBeet(); + + beetRepository.addPlantToRowById(0, PlantGenerator.getPlant()); + + beetRepository.addPlantToRowById(2, PlantGenerator.getPlant()); + beetRepository.addPlantToRowById(2, PlantGenerator.getPlant2()); + }); + + testWidgets('Displays all rows', (WidgetTester tester) async { + final currentDate = DateTime.now(); + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Dashboard( + onPlantDroppedToRow: (rowIndex, plant) {}, + onPlantRemoveFromRow: (rowIndex, plantInRow) {}, + beetRepository: beetRepository, + showSpaceRequirement: false, + showImages: true, + currentDate: currentDate, + ), + ), + ), + ); + + //Assert + expect(find.byType(SingleChildScrollView), findsOneWidget); + expect(find.byType(PlantRow), findsNWidgets(3), reason: '3 Rows are setup'); + + expect(find.byType(PlantRowHorizontalSpace), findsNothing, + reason: 'Vertical Space should be displayed'); + expect(find.byType(PlantRowSpace), findsNothing, + reason: 'Vertical Space should be displayed'); + }); + + testWidgets('Displays all rows show space', (WidgetTester tester) async { + final currentDate = DateTime.now(); + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Dashboard( + onPlantDroppedToRow: (rowIndex, plant) {}, + onPlantRemoveFromRow: (rowIndex, plantInRow) {}, + beetRepository: beetRepository, + showSpaceRequirement: true, + showImages: true, + currentDate: currentDate, + ), + ), + ), + ); + + //Assert + expect(find.byType(SingleChildScrollView), findsOneWidget); + expect(find.byType(PlantRow), findsNWidgets(3), reason: '3 Rows are setup'); + + expect(find.byType(PlantRowHorizontalSpace), findsNWidgets(2), + reason: 'Vertical Space should be displayed'); + expect(find.byType(PlantRowSpace), findsNWidgets(2), + reason: 'Vertical Space should be displayed'); + }); +} diff --git a/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_drop_test.dart b/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_drop_test.dart new file mode 100644 index 0000000..30d7b31 --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_drop_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/plant_drop.dart'; + +import '../../../helpers/plant_generator.dart'; + +void main() { + testWidgets('PlantDrop shold accept Plants', (WidgetTester tester) async { + bool isDropped = false; + Plant plant = PlantGenerator.getPlant(); + Plant? droppedPlant; + + // Arrange + onPlantDropped(Plant plant) { + isDropped = true; + droppedPlant = plant; + } + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Column( + children: [ + Expanded( + child: Draggable( + key: const Key("darg"), + data: plant, + feedback: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + plant.image.toString(), + width: 100, + ), + ], + ), + ), + child: Image.asset( + plant.image.toString(), + width: 100, + ), + ), + ), + Expanded( + child: PlantDrop( + onPlantDropped: onPlantDropped, + showSpaceRequirement: true, + key: const Key("drop"), + )), + ], + )), + ), + ); + + //Act + final TestGesture drag = await tester + .startGesture(tester.getCenter(find.byKey(const Key("darg")))); + await tester.pump(); + await drag.moveTo(tester.getTopLeft(find.byKey(const Key("drop")))); + await drag.up(); + await tester.pumpAndSettle(); + + // Assert + expect(isDropped, equals(true), reason: 'Plant was not dropped'); + expect(droppedPlant, equals(plant), + reason: 'droppedPlant is not the right plant'); + }); +} diff --git a/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_element_test.dart b/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_element_test.dart new file mode 100644 index 0000000..cf73fb8 --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_element_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/entities/plant_in_row.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/plant_element.dart'; + +import '../../../helpers/beet_repository_generator.dart'; +import '../../../helpers/plant_generator.dart'; + +void main() { + testWidgets('PlantElement image onRemovePlant fires', + (WidgetTester tester) async { + bool isRemoved = false; + Plant? plantToRemove; + + // Arrange + onRemovePlant(Plant plant) { + isRemoved = true; + plantToRemove = plant; + } + + final plant = PlantInRow(position: 1, plant: PlantGenerator.getPlant()); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PlantElement( + showImages: true, + onRemovePlant: onRemovePlant, + plant: plant, + beetRepository: BeetRepositoryGenerator.getBeetRepository(), + date: DateTime.now(), + ), + ), + ), + ); + + // Act + final deleteButtonFinder = find.byIcon(Icons.delete); + await tester.tap(deleteButtonFinder); + await tester.pump(); + + //Assert + expect(isRemoved, true, reason: 'Plant is not removed'); + expect(plantToRemove, plant, reason: 'Wrong Plant is returned'); + }); + + testWidgets('PlantElement Text onRemovePlant fires', + (WidgetTester tester) async { + bool isRemoved = false; + Plant? plantToRemove; + + // Arrange + onRemovePlant(Plant plant) { + isRemoved = true; + plantToRemove = plant; + } + + final plant = PlantInRow(position: 1, plant: PlantGenerator.getPlant()); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PlantElement( + showImages: false, + onRemovePlant: onRemovePlant, + plant: plant, + beetRepository: BeetRepositoryGenerator.getBeetRepository(), + date: DateTime.now(), + ), + ), + ), + ); + + // Act + final deleteButtonFinder = find.byIcon(Icons.delete); + await tester.tap(deleteButtonFinder); + await tester.pump(); + + //Assert + expect(isRemoved, true, reason: 'Plant is not removed'); + expect(plantToRemove, plant, reason: 'Wrong Plant is returned'); + }); +} diff --git a/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_row_test.dart b/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_row_test.dart new file mode 100644 index 0000000..36db04c --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/dashboard_widgets/plant_row_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/entities/beet_row.dart'; +import 'package:garden_planner/entities/plant.dart'; +import 'package:garden_planner/entities/plant_in_row.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/plant_element.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/plant_row.dart'; + +import '../../../helpers/beet_repository_generator.dart'; +import '../../../helpers/plant_generator.dart'; + +void main() { + testWidgets('PlantRow displays PlantElement widgets', + (WidgetTester tester) async { + final List plants = [ + PlantInRow(position: 0, plant: PlantGenerator.getPlant()), + PlantInRow(position: 1, plant: PlantGenerator.getPlant2()) + ]; + + final beetRow = BeetRow(0); + beetRow.plants = plants; + + int removePlantCount = 0; + onPlantDropped(Plant plant) {} + onPlantRemove(PlantInRow plantInRow) { + removePlantCount++; + } + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PlantRow( + row: beetRow, + verticalSpace: 10.0, + onPlantDropped: onPlantDropped, + showSpaceRequirement: true, + beetRepository: BeetRepositoryGenerator.getBeetRepository(), + onPlantRemove: onPlantRemove, + showImages: true, + date: DateTime.now(), + ), + ), + ), + ); + + final plantElementFinder = find.byType(PlantElement); + expect(plantElementFinder, findsNWidgets(plants.length), + reason: 'Plants are not added'); + + final deleteButtonFinder = find.byIcon(Icons.delete).first; + await tester.tap(deleteButtonFinder); + await tester.pump(); + + expect(removePlantCount, 1, reason: 'Plant was not removed'); + }); +} diff --git a/garden_planner/test/widgets/content_widgets/dashboard_widgets/space/plant_row_horizontal_space_test.dart b/garden_planner/test/widgets/content_widgets/dashboard_widgets/space/plant_row_horizontal_space_test.dart new file mode 100644 index 0000000..8eae06f --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/dashboard_widgets/space/plant_row_horizontal_space_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/space/plant_row_horizontal_space.dart'; + +void main() { + testWidgets('PlantRowHorizontalSpace displays planting positions', + (WidgetTester tester) async { + // Arrange + final List plantingPositions = [1.0, 2.5, 3.0]; + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PlantRowHorizontalSpace( + plantingPositions: plantingPositions, + ), + ), + ), + ); + + // Assert + // 4 because of 3 positions and 1 plain starting + expect(find.byType(Container), findsNWidgets(4)); + }); + + testWidgets('PlantRowHorizontalSpace displays correct item text', + (WidgetTester tester) async { + // Arrange + final List plantingPositions = [1.5, 2.5, 3.5]; + + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PlantRowHorizontalSpace( + plantingPositions: plantingPositions, + ), + ), + ), + ); + + // Assert + // Skip the first position because it's a placeholder + for (double position in plantingPositions) { + final itemTextFinder = find.text('${position.toStringAsFixed(2)} m'); + expect(itemTextFinder, findsOneWidget); + } + }); +} diff --git a/garden_planner/test/widgets/content_widgets/dashboard_widgets/space/plant_row_space_test.dart b/garden_planner/test/widgets/content_widgets/dashboard_widgets/space/plant_row_space_test.dart new file mode 100644 index 0000000..f4c7652 --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/dashboard_widgets/space/plant_row_space_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/widgets/content_widgets/dashboard_widgets/space/plant_row_space.dart'; + +void main() { + testWidgets('PlantRowSpace displays vertical space', + (WidgetTester tester) async { + const double verticalSpace = 2.5777777; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PlantRowSpace(verticalSpace: verticalSpace), + ), + ), + ); + + // widgets rounds by 2 digets + expect(find.text('${2.58} m'), findsOneWidget, + reason: 'VerticalSpace is nor right'); + }); +} diff --git a/garden_planner/test/widgets/content_widgets/footer_test.dart b/garden_planner/test/widgets/content_widgets/footer_test.dart new file mode 100644 index 0000000..8c1b092 --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/footer_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/repositories/beet.repositories.dart'; +import 'package:garden_planner/widgets/content_widgets/footer.dart'; + +import '../../helpers/beet_repository_generator.dart'; + +void main() { + late BeetRepository beetRepository; + late DateTime currentDate; + late Function(DateTime) mockOnNewDaySelected; + DateTime selectedPickerDate = DateTime(2023, 1, 1); + + setUp(() { + beetRepository = BeetRepositoryGenerator.getBeetRepository(); + currentDate = DateTime.now(); + currentDate = + DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day); + + mockOnNewDaySelected = (DateTime selectedDate) { + selectedPickerDate = selectedDate; + }; + }); + + testWidgets('Displays elements Date select works', + (WidgetTester tester) async { + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Footer( + onNewDaySelected: mockOnNewDaySelected, + beetRepository: beetRepository, + date: currentDate, + ), + ), + ), + ); + + expect(find.byType(Slider), findsOneWidget); + expect(selectedPickerDate, DateTime(2023, 1, 1)); + await tester.tap(find.byType(IconButton)); + await tester.pumpAndSettle(); + await tester.tap(find.text('OK')); + await tester.pumpAndSettle(); + expect(selectedPickerDate, equals(currentDate), reason: 'Datepicker works'); + }); +} diff --git a/garden_planner/test/widgets/content_widgets/new_beet_row_test.dart b/garden_planner/test/widgets/content_widgets/new_beet_row_test.dart new file mode 100644 index 0000000..b8074b2 --- /dev/null +++ b/garden_planner/test/widgets/content_widgets/new_beet_row_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/widgets/content_widgets/new_beet_row.dart'; + +void main() { + testWidgets('NewBeetRow butten press ', (WidgetTester tester) async { + bool newRowPressed = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: NewBeetRow( + onNewRow: () { + newRowPressed = true; + }, + ), + ), + ), + ); + + final buttonFinder = find.byType(ElevatedButton); + await tester.tap(buttonFinder); + expect(newRowPressed, true, reason: 'Button should execute command'); + }); +} diff --git a/garden_planner/test/widgets/header_test.dart b/garden_planner/test/widgets/header_test.dart new file mode 100644 index 0000000..217bd39 --- /dev/null +++ b/garden_planner/test/widgets/header_test.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/constance.dart'; +import 'package:garden_planner/widgets/header.dart'; + +void main() { + testWidgets('Header widget displays correctly', (WidgetTester tester) async { + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: Header( + onSidebarToggle: () {}, + onSave: () {}, + ), + ), + ), + ); + + final sidebarToggleButton = find.byIcon(Icons.menu_open); + final saveButton = find.byIcon(Icons.save); + + //Act + expect(find.text(Constance.apptitle), findsOneWidget, + reason: 'Title is missing'); + expect(sidebarToggleButton, findsOneWidget, + reason: 'SidbarToggel is missing'); + expect(saveButton, findsOneWidget, reason: 'Savebutton is missing'); + }); + + testWidgets('Header widget save abort should work', + (WidgetTester tester) async { + bool savePressed = false; + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: Header( + onSidebarToggle: () {}, + onSave: () { + savePressed = true; + }, + ), + ), + ), + ); + + final saveButton = find.byIcon(Icons.save); + + //Act + await tester.tap(saveButton); + await tester.pumpAndSettle(); + + final saveDialog = find.byType(AlertDialog); + expect(saveDialog, findsOneWidget); + + // Test false + final cancelButton = find.text('Abbrechen'); + await tester.tap(cancelButton); + await tester.pumpAndSettle(); + + //Assert + expect(savePressed, false, reason: 'save was not confirmed but saved'); + }); + + testWidgets('Header widget save ok should work', (WidgetTester tester) async { + bool savePressed = false; + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: Header( + onSidebarToggle: () {}, + onSave: () { + savePressed = true; + }, + ), + ), + ), + ); + + final saveButton = find.byIcon(Icons.save); + + //Act + await tester.tap(saveButton); + await tester.pumpAndSettle(); + + final saveDialog = find.byType(AlertDialog); + expect(saveDialog, findsOneWidget); + + final saveOkButton = find.text('Ja'); + await tester.tap(saveOkButton); + await tester.pumpAndSettle(); + + //Assert + expect(savePressed, true, reason: 'Save confirmed but not saved'); + }); + + testWidgets('Header widget sidbar toggle should work', + (WidgetTester tester) async { + bool sidebarToggle = false; + + //Arrange + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: Header( + onSidebarToggle: () { + sidebarToggle = true; + }, + onSave: () {}, + ), + ), + ), + ); + + final sidebarToggleButton = find.byIcon(Icons.menu_open); + + //Act + await tester.tap(sidebarToggleButton); + await tester.pumpAndSettle(); + + //Assert + expect(sidebarToggle, true); + }); +} diff --git a/garden_planner/test/widgets/sidebar_test.dart b/garden_planner/test/widgets/sidebar_test.dart new file mode 100644 index 0000000..6053de9 --- /dev/null +++ b/garden_planner/test/widgets/sidebar_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:garden_planner/repositories/beet.repositories.dart'; +import 'package:garden_planner/widgets/sidebar.dart'; + +import '../helpers/beet_repository_generator.dart'; + +void main() { + testWidgets('Sidebar widget displays all items', (WidgetTester tester) async { + //Arrange + BeetRepository repo = BeetRepositoryGenerator.getBeetRepository(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Sidebar( + beetRepository: repo, + ), + ), + ), + ); + + //Act + await tester.pumpAndSettle(const Duration(seconds: 5)); + + //Assert + expect(find.text('Verfügbare Pflanzen'), findsOneWidget); + expect(find.byType(Divider), findsOneWidget); + expect(find.byType(Image), findsNWidgets(2), + reason: 'The image of the plants should be displayed'); + }); +} diff --git a/garden_planner/web/favicon.png b/garden_planner/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/garden_planner/web/favicon.png differ diff --git a/garden_planner/web/icons/Icon-192.png b/garden_planner/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/garden_planner/web/icons/Icon-192.png differ diff --git a/garden_planner/web/icons/Icon-512.png b/garden_planner/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/garden_planner/web/icons/Icon-512.png differ diff --git a/garden_planner/web/icons/Icon-maskable-192.png b/garden_planner/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/garden_planner/web/icons/Icon-maskable-192.png differ diff --git a/garden_planner/web/icons/Icon-maskable-512.png b/garden_planner/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/garden_planner/web/icons/Icon-maskable-512.png differ diff --git a/garden_planner/web/index.html b/garden_planner/web/index.html new file mode 100644 index 0000000..a7a57b9 --- /dev/null +++ b/garden_planner/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + garden_planner + + + + + + + + + + diff --git a/garden_planner/web/manifest.json b/garden_planner/web/manifest.json new file mode 100644 index 0000000..150b049 --- /dev/null +++ b/garden_planner/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "garden_planner", + "short_name": "garden_planner", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/garden_planner/windows/.gitignore b/garden_planner/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/garden_planner/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/garden_planner/windows/CMakeLists.txt b/garden_planner/windows/CMakeLists.txt new file mode 100644 index 0000000..bffe8d9 --- /dev/null +++ b/garden_planner/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(garden_planner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "garden_planner") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/garden_planner/windows/flutter/CMakeLists.txt b/garden_planner/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..930d207 --- /dev/null +++ b/garden_planner/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/garden_planner/windows/runner/CMakeLists.txt b/garden_planner/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/garden_planner/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/garden_planner/windows/runner/Runner.rc b/garden_planner/windows/runner/Runner.rc new file mode 100644 index 0000000..2818ab1 --- /dev/null +++ b/garden_planner/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "garden_planner" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "garden_planner" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "garden_planner.exe" "\0" + VALUE "ProductName", "garden_planner" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/garden_planner/windows/runner/flutter_window.cpp b/garden_planner/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..b25e363 --- /dev/null +++ b/garden_planner/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/garden_planner/windows/runner/flutter_window.h b/garden_planner/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/garden_planner/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/garden_planner/windows/runner/main.cpp b/garden_planner/windows/runner/main.cpp new file mode 100644 index 0000000..96bf331 --- /dev/null +++ b/garden_planner/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"garden_planner", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/garden_planner/windows/runner/resource.h b/garden_planner/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/garden_planner/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/garden_planner/windows/runner/resources/app_icon.ico b/garden_planner/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/garden_planner/windows/runner/resources/app_icon.ico differ diff --git a/garden_planner/windows/runner/runner.exe.manifest b/garden_planner/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/garden_planner/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/garden_planner/windows/runner/utils.cpp b/garden_planner/windows/runner/utils.cpp new file mode 100644 index 0000000..f5bf9fa --- /dev/null +++ b/garden_planner/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/garden_planner/windows/runner/utils.h b/garden_planner/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/garden_planner/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/garden_planner/windows/runner/win32_window.cpp b/garden_planner/windows/runner/win32_window.cpp new file mode 100644 index 0000000..041a385 --- /dev/null +++ b/garden_planner/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/garden_planner/windows/runner/win32_window.h b/garden_planner/windows/runner/win32_window.h new file mode 100644 index 0000000..c86632d --- /dev/null +++ b/garden_planner/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..b56eea9 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,16 @@ +# SonarQube server +# sonar.host.url & sonar.login are set by the Scanner CLI. +# See https://docs.sonarqube.org/latest/analysis/gitlab-cicd/. + +# Project settings. +sonar.projectKey=Joerg_cpd_project +sonar.projectName=CPD Project +sonar.projectDescription=Flutter Project for CPD +sonar.links.ci=https://gitlab.vierling.cloud/Joerg/cpd_project/pipelines + +sonar.sources=garden_planner/lib +sonar.tests=garden_planner/test +sonar.sourceEncoding=UTF-8 + +# Fail CI pipeline if Sonar fails. +sonar.qualitygate.wait=true diff --git a/wireframes/Add_Plant.jpg b/wireframes/Add_Plant.jpg new file mode 100644 index 0000000..f422af9 Binary files /dev/null and b/wireframes/Add_Plant.jpg differ diff --git a/wireframes/Common_Details.jpg b/wireframes/Common_Details.jpg new file mode 100644 index 0000000..68abe7b Binary files /dev/null and b/wireframes/Common_Details.jpg differ diff --git a/wireframes/Main.jpg b/wireframes/Main.jpg new file mode 100644 index 0000000..38924e7 Binary files /dev/null and b/wireframes/Main.jpg differ diff --git a/wireframes/View_Space.jpg b/wireframes/View_Space.jpg new file mode 100644 index 0000000..09ddfa3 Binary files /dev/null and b/wireframes/View_Space.jpg differ