Base Verion

main
joerg 2023-06-25 10:13:39 +02:00
commit 2a8805f801
152 changed files with 7340 additions and 0 deletions

138
.gitignore vendored 100644
View File

@ -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

35
.gitlab-ci.yml 100644
View File

@ -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

BIN
CPD Idee.msg 100644

Binary file not shown.

36
README.md 100644
View File

@ -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"

11
api/Dockerfile 100644
View File

@ -0,0 +1,11 @@
FROM node:latest
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

113
api/init.sql 100644
View File

@ -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');

811
api/package-lock.json generated 100644
View File

@ -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"
}
}
}
}

18
api/package.json 100644
View File

@ -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"
}
}

126
api/server.js 100644
View File

@ -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}`);
});

View File

@ -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

View File

@ -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'

View File

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:

View File

@ -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

View File

@ -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"
}

View File

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.garden_planner">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,35 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.garden_planner">
<application
android:label="garden_planner"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2"/>
</application>
</manifest>

View File

@ -0,0 +1,6 @@
package com.example.cpd_project
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
}

View File

@ -0,0 +1,6 @@
package com.example.garden_planner
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground"/>
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white"/>
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS'plant_drop.dart Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter'plant_drop.dart Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS'plant_drop.dart Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter'plant_drop.dart Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.garden_planner">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -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
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1,13 @@
class BeetEntry {
final int plantId;
final int position;
final int beetRow;
BeetEntry(this.plantId, this.position, this.beetRow);
Map<String, dynamic> toJson() => {
'plantId': plantId,
'position': position,
'beet_row': beetRow,
};
}

View File

@ -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<List<Plant>> getAllAvailablePlants() async {
final response = await httpConnector.getAllPlants();
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map<Plant>((plantData) => convertPlant(plantData)).toList();
} else {
throw Exception('Failed to fetch plants');
}
}
Future<String> 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<List<BeetApiEntryReturn>> getBeet() async {
final response = await httpConnector.getBeet();
if (response.statusCode == 200) {
final List<dynamic> sortedBeetEntriesFormDB =
_sortBeetFromDB(response.body);
final List<BeetApiEntryReturn> 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<dynamic> _sortBeetFromDB(String response) {
final List<dynamic> 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<Plant> 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<dynamic> 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<BeetEntry> _generateBeetEntries(Beet beet) {
final entries = <BeetEntry>[];
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;
}
}

View File

@ -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<http.Response> getAllPlants() async {
final url = Uri.parse('$_apiLocation/plants');
final response = await http.get(url);
return response;
}
Future<http.Response> getPlant(int id) async {
final url = Uri.parse('$_apiLocation/plants/$id');
final response = await http.get(url);
return response;
}
Future<http.Response> getBeet() async {
final url = Uri.parse('$_apiLocation/beet');
final response = await http.get(url);
return response;
}
Future<http.Response> 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View File

@ -0,0 +1,50 @@
import 'plant.dart';
class Beet {
List<BeetRow> beetrows= [BeetRow()];
void Add(BeetRow beetrow){
beetrows.add(beetrow);
}
}
class BeetRow {
List<Plant> 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;
}

View File

@ -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;
}

View File

@ -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<Content> {
Beet beet = Beet();
List<Widget> getRows(Beet beet) {
List<Widget> displayedRows = [];
List<Widget> verticalSpaceContainers = [];
verticalSpaceContainers=getVerticalSpaceContainers(beet);
for(int i =0; i<beet.beetrows.length;i++) {
var beetRow = beet.beetrows[i];
Widget? verticalSpace = null;
if (widget.showSpaceRequirement && beetRow.plants.isNotEmpty) {
displayedRows.add(getHorizontalSpaceRow(beetRow));
verticalSpace=verticalSpaceContainers[i];
}
displayedRows.add(getPlantRow(beetRow, verticalSpace));
}
return displayedRows;
}
List<double> getHorizontalSpaceValue(BeetRow beetRow) {
double preUsedSpace = 0;
List<double> spaceElements = [];
for (var plant in beetRow.plants) {
spaceElements.add(preUsedSpace + (plant.horizontalSpace / 2));
preUsedSpace += plant.horizontalSpace;
}
return spaceElements;
}
List<double> getVerticalSpaceValue(Beet beet) {
double preUsedSpace = 0;
List<double> 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<Container> getVerticalSpaceContainers(Beet beet) {
var requiredSpaceValues = getVerticalSpaceValue(beet);
List<Container> 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<Plant>(
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: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...getRows(beet),
ElevatedButton(
onPressed: () {
setState(() {
beet.Add(BeetRow());
});
},
child: Text('Neue Reihe'),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class ContentControl extends StatelessWidget {
final bool showSpaceRequirements;
final ValueChanged<bool> 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'),
]);
}
}

View File

@ -0,0 +1,15 @@
import 'package:garden_planner/entities/beet_row.dart';
class Beet {
int _internalRow = 0;
List<BeetRow> beetRows = [];
Beet() {
addNewRow();
}
void addNewRow() {
beetRows.add(BeetRow(_internalRow));
_internalRow += 1;
}
}

View File

@ -0,0 +1,8 @@
import 'package:garden_planner/entities/plant.dart';
class BeetApiEntryReturn {
final Plant plant;
final int beetRow;
BeetApiEntryReturn(this.plant, this.beetRow);
}

View File

@ -0,0 +1,8 @@
import 'package:garden_planner/entities/plant_in_row.dart';
class BeetRow {
int id;
List<PlantInRow> plants = [];
BeetRow(this.id);
}

View File

@ -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<PlantTime> times = [];
Plant({
String? imagePath,
List<PlantTime>? 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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});
}

View File

@ -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()
],
);
}
}

View File

@ -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
},
),
],
);
}
}

View File

@ -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<double> getVerticalPlantingSpace(
final BeetRowService beetRowService, final Beet beet) {
double preUsedSpace = 0;
final List<double> 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<String> saveBeet(
final GardenApiService apiService, final Beet beet) async {
return apiService.saveBeet(beet);
}
Future<void> loadBeet(final GardenApiService apiService,
final BeetRowService rowService, final Beet beet) async {
final List<BeetApiEntryReturn> 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);
}
}

View File

@ -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<double> getHorizontalPlantingPosition(final BeetRow beetRow) {
double preUsedSpace = 0;
List<double> 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);
}
}

View File

@ -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;
}
}

View File

@ -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<PlantTime> _getPlantTimes(final Plant plant, final DateTime date) {
return plant.times
.where((time) => DateHelper.isDateBetween(date, time.from, time.until))
.toList();
}
}

View File

@ -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<GardenPlanner> {
bool _isSidebarOpen = Constance.sidebarAtStart;
@override
void initState() {
super.initState();
loadBeet();
}
Future<void> loadBeet() async {
await widget.beetRepository.loadBeet();
setState(() {});
}
Future<void> 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: <Widget>[
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<DragAndDropScreen> {
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: <Widget>[
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

View File

@ -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,
});
}

View File

@ -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<double> getVerticalSpaceValue() {
return beetService.getVerticalPlantingSpace(beetRowService, beet);
}
void removePlantFromRowById(int rowId, int plantInRowId) {
var row = beetRowService.getRowById(beet, rowId);
beetRowService.removePlantFromRowById(row, plantInRowId);
}
Future<void> saveBeet() async {
_messages = await beetService.saveBeet(apiService, beet);
}
Future<Iterable<Plant>> 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<void> loadBeet() async {
await beetService.loadBeet(apiService, beetRowService, beet);
_messages = "Beet geladen";
}
List<double> getHorizontalPlantingPositionForRow(BeetRow beetRow) {
return beetRowService.getHorizontalPlantingPosition(beetRow);
}
BeetRow getRow(int index) {
return beet.beetRows[index];
}
}

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'plant.dart';
class Sidebar extends StatelessWidget {
List<Plant> 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<Plant>(
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(),
)
]
);
}
}

View File

@ -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<Content> {
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,
)
],
);
}
}

View File

@ -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<Widget> _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)),
);
}
}

View File

@ -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,
);
},
),
],
),
),
],
),
),
);
}
}

View File

@ -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<Plant>(
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,
)),
),
],
),
);
},
);
}
}

View File

@ -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);
},
);
}
}

View File

@ -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)
]);
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
class PlantRowHorizontalSpace extends StatelessWidget {
final List<double> 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),
),
],
),
);
}
}

View File

@ -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),
),
],
),
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class PlantRowHorizontalWater extends StatelessWidget {
final List<double> 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)),
],
),
)
]);
}
}

View File

@ -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)),
],
),
);
}
}

View File

@ -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);
}
}
}

View File

@ -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'),
),
);
}
}

View File

@ -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,
),
)
)
],
);
}
}

View File

@ -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<Iterable<Plant>>(
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<Plant>(
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');
}
},
),
),
],
);
}
}

View File

@ -0,0 +1 @@
flutter/ephemeral

View File

@ -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 "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>: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()

View File

@ -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}
)

View File

@ -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);
}

View File

@ -0,0 +1,104 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#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));
}

View File

@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
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_

View File

@ -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/

View File

@ -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));
});
}

View File

@ -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<Map<String, dynamic>>());
expect(jsonMap['plantId'], equals(1));
expect(jsonMap['position'], equals(2));
expect(jsonMap['beet_row'], equals(3));
});
}

View File

@ -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');
}
});
}

View File

@ -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');
});
}

Some files were not shown because too many files have changed in this diff Show More