Projekt-Gerüst: Flutter-App mit Heute-Checkliste, Pflanzenverwaltung, Stellplätzen und Einstellungen

- Flutter/Dart (iOS + Android), Riverpod, go_router, i18n via ARB (deutsch)
- Feature-Struktur: today, plants, locations, household (V2-Platzhalter), settings
- Intervall-Modell: Aufgaben werden aus lastWatered/lastFertilized + Intervall berechnet
- In-Memory-Demo-Daten, Firebase-Anbindung folgt im nächsten Block
- Doku: Architektur, Firebase-Einrichtung, Forgejo-Git-Hosting

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cschlaefke 2026-07-18 21:13:18 +02:00
commit 6e22d9c3aa
93 changed files with 4948 additions and 0 deletions

View file

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(flutter pub *)",
"Bash(flutter analyze *)"
]
}
}

45
.gitignore vendored Normal file
View file

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

33
.metadata Normal file
View file

@ -0,0 +1,33 @@
# 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 and should not be manually edited.
version:
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: android
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: ios
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
# 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'

39
README.md Normal file
View file

@ -0,0 +1,39 @@
# Planty 🌱
App zur Verwaltung und Pflege von Pflanzen — für den eigenen Haushalt und Pflanzen-Sitter.
## Status
**V1 in Entwicklung.** Aktueller Stand: App-Gerüst mit Tages-Checkliste, Pflanzenverwaltung, Stellplätzen und Einstellungen (In-Memory-Demo-Daten, noch ohne Firebase-Anbindung).
## Roadmap
| Version | Inhalt |
| --- | --- |
| **V1** | Pflanze anlegen (Foto → PlantNet-Erkennung → Claude-Pflegeprofil), Gieß-/Düngeplan, Tages-Checkliste mit Bestätigung, tägliche Push-Erinnerung |
| **V2** | Haushalt teilen per Einladung, Sitter-Rolle, gemeinsame Bestätigungen |
| **V3** | Krankheits-Diagnose per Foto, Stellplatz-Bewertung, Umtopf-Erinnerungen |
## Entwicklung
```bash
flutter pub get # Abhängigkeiten laden
flutter gen-l10n # Übersetzungen generieren (nach Änderungen an lib/l10n/*.arb)
flutter run # App auf Gerät/Simulator starten
flutter test # Tests ausführen
flutter analyze # Statische Analyse
```
## Dokumentation
- [Architektur & Projektstruktur](docs/architektur.md)
- [Firebase-Einrichtung (Schritt für Schritt)](docs/firebase-einrichtung.md)
- [Git-Hosting mit Forgejo auf dem Hetzner-VPS](docs/git-forgejo.md)
## Technik-Überblick
- **Flutter/Dart** (iOS + Android), State Management mit **Riverpod**, Navigation mit **go_router**
- **Firebase** (Auth, Firestore, Storage, Cloud Functions, FCM) — Anbindung folgt im nächsten Block
- **KI hybrid:** PlantNet (kostenlose Arterkennung) + Claude (Pflegetexte, Diagnose) über Cloud Functions
- **i18n:** Alle Texte liegen in `lib/l10n/app_de.arb`; Englisch ist später nur eine zweite ARB-Datei
- Bundle-ID: vorläufig `dev.planty.app` — wird vor dem ersten TestFlight-Upload final gesetzt

28
analysis_options.yaml Normal file
View file

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View file

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "dev.planty.planty"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "dev.planty.planty"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
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.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 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,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="planty"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
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>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,5 @@
package dev.planty.planty
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 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's 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's 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's 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's 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,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 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>

24
android/build.gradle.kts Normal file
View file

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View file

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=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-8.14-all.zip

View file

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

60
docs/architektur.md Normal file
View file

@ -0,0 +1,60 @@
# Architektur & Projektstruktur
Dieses Dokument erklärt den Aufbau des Planty-Codes — auch als Blaupause für weitere Flutter-Apps.
## Ordnerstruktur
```
lib/
├── main.dart # Einstiegspunkt: lädt SharedPreferences, startet die App
├── app.dart # MaterialApp: Theme, Routing, Lokalisierung, Schriftgröße
├── core/ # Alles, was NICHT zu einem einzelnen Feature gehört
│ ├── router/app_router.dart # Zentrale Routen-Definition (go_router)
│ ├── settings/ # App-Einstellungen (Theme, Schriftgröße, Erinnerungszeit)
│ ├── theme/app_theme.dart # Farbschema und Bedienbarkeits-Anpassungen
│ └── widgets/app_drawer.dart# Das Hamburger-Menü
├── features/ # Ein Ordner pro Fachlichkeit
│ ├── today/ # Tages-Checkliste (Start-Screen)
│ ├── plants/ # Pflanzen: Liste, Profil, Anlegen/Bearbeiten
│ ├── locations/ # Stellplätze
│ ├── household/ # Haushalt (V2-Platzhalter)
│ └── settings/ # Einstellungs-Screen
└── l10n/
├── app_de.arb # ALLE deutschen Texte (Quelle)
└── generated/ # Automatisch generierter Code (nicht anfassen)
```
## Schichten innerhalb eines Features
Jedes Feature folgt demselben Muster (nicht jedes braucht alle Schichten):
- **`domain/`** — Die fachlichen Modelle (`Plant`, `DueTask`, `PlantLocation`). Reines Dart, keine Flutter- oder Firebase-Abhängigkeiten. Hier steckt die Fachlogik (z. B. „Fälligkeit = letzte Erledigung + Intervall").
- **`data/`** — Datenhaltung. Aktuell In-Memory-Notifier mit Demo-Daten; im Firebase-Block werden sie durch Firestore-Anbindungen ersetzt. **Wichtig:** Die Provider-Schnittstelle nach außen bleibt gleich — die Screens merken vom Austausch nichts.
- **`application/`** — Abgeleitete Zustände / Anwendungslogik, z. B. `dueTasksProvider`, der aus den Pflanzen die heute fälligen Aufgaben berechnet.
- **`presentation/`** — Die Screens und Widgets.
**Faustregel für Abhängigkeiten:** `presentation → application → data → domain`. Niemals andersherum, und Features greifen nur über Provider aufeinander zu.
## State Management: Riverpod in 3 Sätzen
1. Ein **`Notifier`** hält veränderlichen Zustand (z. B. die Pflanzenliste) und bietet Methoden zum Ändern (`addPlant`, `confirmTask`); ein **`Provider`** berechnet abgeleiteten Zustand (z. B. fällige Aufgaben) und aktualisiert sich automatisch, wenn sich seine Quellen ändern.
2. Screens sind `ConsumerWidget`s: `ref.watch(provider)` liest den Zustand und baut die UI bei Änderungen neu; `ref.read(provider.notifier).methode()` löst Änderungen aus (in Callbacks).
3. `ProviderScope` in `main.dart` ist die Wurzel; dort werden auch echte Abhängigkeiten (SharedPreferences, später Firebase) hineingereicht — Tests können sie durch Mocks ersetzen.
## Kernentscheidungen im fachlichen Modell
- **Aufgaben werden nicht gespeichert, sondern berechnet.** Es gibt keine „Task-Tabelle": Aus `lastWatered + wateringIntervalDays` ergibt sich die Fälligkeit. Eine Bestätigung setzt nur `lastWatered` neu — dadurch kann nichts inkonsistent werden, und Überfälliges „wandert" automatisch mit.
- **Haushaltszentriert gedacht:** Pflanzen, Stellplätze und Aufgaben gehören dem *Haushalt*, nicht einem Nutzer. In Firestore wird das `households/{id}/plants/{id}` usw. — so ist V2 (Sharing) nur „weiteres Mitglied hinzufügen", keine Datenmigration.
- **`null` bei `lastWatered` heißt „noch nie"** → Aufgabe ist sofort fällig. So braucht eine neu angelegte Pflanze keine Sonderbehandlung.
## Übersetzungen (i18n)
Alle Texte stehen in `lib/l10n/app_de.arb` als Schlüssel-Wert-Paare. Im Code heißt es nie `Text('Gießen')`, sondern `Text(l10n.taskWaterTitle(name))`. Englisch später hinzufügen = eine Datei `app_en.arb` mit denselben Schlüsseln anlegen und `flutter gen-l10n` ausführen — kein Code-Umbau.
Nach jeder Änderung an der `.arb`-Datei: `flutter gen-l10n` ausführen (passiert bei `flutter run` auch automatisch).
## Bedienbarkeit (Zielgruppe: auch ältere Menschen)
- Schriftgröße in den Einstellungen wirkt **zusätzlich** zur System-Schriftgröße (`app.dart`, `builder`).
- „Hoher Kontrast" nutzt Material 3 `contrastLevel` — dasselbe Farbschema, kräftigere Kontraste.
- Große Touch-Ziele (`app_theme.dart`), keine Swipe-Gesten, alle Funktionen über sichtbare Buttons und das Hamburger-Menü.

View file

@ -0,0 +1,67 @@
# Firebase-Einrichtung — Schritt für Schritt
Firebase ist Googles „Backend als Dienst": Es liefert uns Login (**Auth**), Datenbank mit Echtzeit-Sync (**Firestore**), Foto-Speicher (**Storage**), serverseitigen Code (**Cloud Functions**) und Push-Nachrichten (**FCM**) — ohne dass wir einen eigenen Server betreiben.
> **Stand:** Vorbereitung. Die App läuft aktuell noch mit Demo-Daten ohne Firebase. Dieses Dokument wird mit jedem Firebase-Baustein erweitert und führt dich durch alles, was du selbst (mit deinem Google-Konto) tun musst.
## Was wofür? (Überblick)
| Baustein | Aufgabe in Planty |
| --- | --- |
| **Authentication** | Login mit E-Mail+Passwort, Google und Apple |
| **Cloud Firestore** | Datenbank: Haushalte, Pflanzen, Stellplätze, Bestätigungen — synchronisiert in Echtzeit auf alle Geräte des Haushalts |
| **Storage** | Speichert die Pflanzen- und Stellplatz-Fotos |
| **Cloud Functions** | Unser Server-Code: ruft PlantNet/Claude auf (damit API-Keys nie in der App stecken) und verschickt die täglichen Erinnerungen |
| **Cloud Messaging (FCM)** | Stellt die Push-Nachrichten auf dem Sperrbildschirm zu |
## Schritt 1: Firebase-Projekt anlegen (machst du, ~10 Minuten)
1. Öffne <https://console.firebase.google.com> und melde dich mit deinem Google-Konto an.
2. **„Projekt hinzufügen"** → Name: `planty` → Google Analytics kannst du **deaktivieren** (brauchen wir nicht).
3. Nach dem Anlegen bist du in der Projekt-Übersicht. Fertig für heute — Apps registrieren wir gemeinsam per CLI (Schritt 3).
## Schritt 2: Auf den Blaze-Tarif umstellen (machst du, ~5 Minuten)
Cloud Functions (unser Server-Code) setzen den **Blaze-Tarif** („Pay as you go") voraus. Keine Sorge vor Kosten:
- Die großzügigen **Gratis-Kontingente bleiben auch im Blaze-Tarif bestehen** (z. B. 2 Mio. Function-Aufrufe/Monat, 50.000 Firestore-Lesezugriffe/Tag). Bei ~5 Nutzern bleiben wir weit darunter → real **~0 €/Monat**.
- In der Console: Zahnrad → **Nutzung und Abrechnung****Tarif ändern** → Blaze → Kreditkarte hinterlegen.
**Kostenschutz einrichten (wichtig, machen wir direkt mit):**
1. In der Google Cloud Console (<https://console.cloud.google.com>, gleiches Projekt) → **Abrechnung → Budgets und Benachrichtigungen**.
2. Budget anlegen: z. B. **5 €/Monat**, E-Mail-Alarm bei 50 %, 90 % und 100 %.
3. So bekommst du eine Mail, lange bevor irgendetwas teuer wird.
## Schritt 3: App mit Firebase verbinden (machen wir gemeinsam per CLI)
Wenn Schritt 12 erledigt sind, läuft das so (führe ich mit dir aus, hier zur Doku):
```bash
# Firebase-CLI installieren und einloggen (öffnet den Browser)
npm install -g firebase-tools
firebase login
# FlutterFire-CLI installieren
dart pub global activate flutterfire_cli
# Verbindet das Flutter-Projekt mit dem Firebase-Projekt:
# registriert die iOS- und Android-App und erzeugt lib/firebase_options.dart
flutterfire configure
```
`flutterfire configure` erzeugt `lib/firebase_options.dart` — darin stehen die Projekt-Kennungen (keine Geheimnisse, darf ins Git). Die eigentlichen API-Keys (Anthropic, PlantNet) leben **nur** in der Cloud-Functions-Umgebung, niemals in der App.
## Schritt 4 und folgende (kommen mit den nächsten Blöcken)
- **Firestore-Datenmodell + Sicherheitsregeln** — wer darf was lesen/schreiben (Haushalts-Mitgliedschaft), Doku folgt beim Firestore-Block.
- **Auth einrichten** — E-Mail+Passwort, Google, Apple (Apple-Login braucht den Apple Developer Account).
- **Cloud Functions** — PlantNet-/Claude-Anbindung und die geplante Erinnerungs-Function (läuft alle 15 Minuten).
- **FCM** — Push-Einrichtung inkl. APNs-Schlüssel für iOS (aus dem Apple Developer Account).
## Begriffe kurz erklärt
- **Projekt:** Der Container für alles — eine App = ein Firebase-Projekt.
- **Firestore-Dokument/Collection:** Firestore ist keine Tabellen-Datenbank, sondern speichert JSON-artige *Dokumente* in *Collections* (Ordnern), z. B. `households/abc123/plants/xyz789`.
- **Security Rules:** Regeln auf dem Server, die festlegen, wer welche Dokumente lesen/schreiben darf — unser Schutz, obwohl die App direkt mit der Datenbank spricht.
- **Cloud Function:** Eine Funktion (bei uns TypeScript), die bei Ereignissen oder nach Zeitplan auf Google-Servern läuft.

58
docs/git-forgejo.md Normal file
View file

@ -0,0 +1,58 @@
# Git-Hosting mit Forgejo auf dem Hetzner-VPS
Forgejo ist ein leichtgewichtiger, quelloffener Git-Server mit Web-Oberfläche (Fork von Gitea): Repositories, Issues, Releases — wie ein privates GitHub auf deinem eigenen Server.
## 1. Forgejo per Docker Compose installieren (auf dem VPS)
Auf dem VPS z. B. unter `/opt/forgejo/docker-compose.yml`:
```yaml
services:
forgejo:
image: codeberg.org/forgejo/forgejo:12
container_name: forgejo
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000" # Web-UI (dahinter Reverse-Proxy mit HTTPS setzen)
- "2222:22" # SSH für git push/pull
```
```bash
cd /opt/forgejo && docker compose up -d
```
Dann `http://<vps-ip>:3000` öffnen → Ersteinrichtung durchklicken (SQLite als Datenbank reicht völlig) → Admin-Konto anlegen.
**Empfohlen:** Wie bei deinen anderen Diensten einen Reverse-Proxy (Caddy/Traefik/nginx) mit HTTPS davorschalten, z. B. `git.deine-domain.de`, und Port 3000 nicht öffentlich lassen. In den Forgejo-Einstellungen die Registrierung deaktivieren (`Site Administration → Configuration`), da nur du den Server nutzt.
## 2. SSH-Schlüssel hinterlegen (auf dem Mac)
```bash
# Vorhandenen Schlüssel anzeigen (oder mit ssh-keygen -t ed25519 neu erzeugen)
cat ~/.ssh/id_ed25519.pub
```
Inhalt in Forgejo unter **Einstellungen → SSH-Schlüssel** einfügen.
## 3. Repository anlegen und Planty pushen
In der Forgejo-Web-UI: **Neues Repository** → Name `planty`**ohne** README/Lizenz initialisieren (das Repo existiert lokal schon). Dann auf dem Mac:
```bash
cd ~/Documents/Projekte/Planty
git remote add origin ssh://git@<vps-host>:2222/<dein-nutzer>/planty.git
git push -u origin main
```
Ab dann genügt nach jedem Arbeitsstand ein `git push`.
## 4. Backup nicht vergessen
Der Ordner `/opt/forgejo/data` enthält alles (Repos + Datenbank). Nimm ihn in dein bestehendes VPS-Backup auf — zusammen mit der lokalen Kopie auf dem Mac hast du dann immer zwei vollständige Stände.

34
ios/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View file

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View file

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Normal file
View file

@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View file

@ -0,0 +1,619 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = KSX6GJY9C5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.planty.planty;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.planty.planty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.planty.planty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.planty.planty.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = KSX6GJY9C5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.planty.planty;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = KSX6GJY9C5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.planty.planty;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View file

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Planty</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>planty</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View file

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

5
l10n.yaml Normal file
View file

@ -0,0 +1,5 @@
arb-dir: lib/l10n
template-arb-file: app_de.arb
output-localization-file: app_localizations.dart
output-dir: lib/l10n/generated
nullable-getter: false

44
lib/app.dart Normal file
View file

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/router/app_router.dart';
import 'core/settings/settings_provider.dart';
import 'core/theme/app_theme.dart';
import 'l10n/generated/app_localizations.dart';
class PlantyApp extends ConsumerWidget {
const PlantyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(settingsProvider);
final router = ref.watch(appRouterProvider);
return MaterialApp.router(
routerConfig: router,
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
theme: buildAppTheme(
brightness: Brightness.light,
highContrast: settings.highContrast,
),
darkTheme: buildAppTheme(
brightness: Brightness.dark,
highContrast: settings.highContrast,
),
themeMode: settings.themeMode,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) {
// App-Schriftgröße wird ZUSÄTZLICH zur System-Schriftgröße angewendet,
// damit die Systemeinstellung des Nutzers respektiert bleibt.
final systemScale = MediaQuery.textScalerOf(context).scale(1.0);
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(systemScale * settings.textSize.factor),
),
child: child!,
);
},
);
}
}

View file

@ -0,0 +1,65 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/household/presentation/household_screen.dart';
import '../../features/locations/presentation/locations_screen.dart';
import '../../features/plants/domain/plant.dart';
import '../../features/plants/presentation/plant_detail_screen.dart';
import '../../features/plants/presentation/plant_form_screen.dart';
import '../../features/plants/presentation/plants_screen.dart';
import '../../features/settings/presentation/settings_screen.dart';
import '../../features/today/presentation/today_screen.dart';
/// Zentrale Routen-Definition. Die Pfade werden auch vom Drawer genutzt,
/// deshalb hier als Konstanten.
abstract final class AppRoutes {
static const today = '/';
static const plants = '/plants';
static const plantAdd = '/plants/add';
static const plantEdit = '/plants/edit';
static const locations = '/locations';
static const household = '/household';
static const settings = '/settings';
}
final appRouterProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: AppRoutes.today,
routes: [
GoRoute(
path: AppRoutes.today,
builder: (context, state) => const TodayScreen(),
),
GoRoute(
path: AppRoutes.plants,
builder: (context, state) => const PlantsScreen(),
),
GoRoute(
path: AppRoutes.plantAdd,
builder: (context, state) => const PlantFormScreen(),
),
GoRoute(
path: AppRoutes.plantEdit,
builder: (context, state) =>
PlantFormScreen(existing: state.extra as Plant?),
),
GoRoute(
path: '/plants/:id',
builder: (context, state) =>
PlantDetailScreen(plantId: state.pathParameters['id']!),
),
GoRoute(
path: AppRoutes.locations,
builder: (context, state) => const LocationsScreen(),
),
GoRoute(
path: AppRoutes.household,
builder: (context, state) => const HouseholdScreen(),
),
GoRoute(
path: AppRoutes.settings,
builder: (context, state) => const SettingsScreen(),
),
],
);
});

View file

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Wird in main.dart mit der echten Instanz überschrieben.
final sharedPreferencesProvider = Provider<SharedPreferences>(
(ref) => throw UnimplementedError('sharedPreferencesProvider muss in main.dart überschrieben werden'),
);
/// Schriftgrößen-Stufen, die der Nutzer in den Einstellungen wählen kann.
/// Der Faktor wird zusätzlich zur System-Schriftgröße angewendet.
enum AppTextSize {
normal(1.0),
large(1.15),
extraLarge(1.3);
const AppTextSize(this.factor);
final double factor;
}
@immutable
class AppSettings {
const AppSettings({
required this.themeMode,
required this.textSize,
required this.highContrast,
required this.reminderTime,
});
final ThemeMode themeMode;
final AppTextSize textSize;
final bool highContrast;
/// Uhrzeit der täglichen Sammel-Erinnerung. Wird lokal gespeichert und
/// später mit dem Firebase-Backend synchronisiert (V1-Push-Block).
final TimeOfDay reminderTime;
AppSettings copyWith({
ThemeMode? themeMode,
AppTextSize? textSize,
bool? highContrast,
TimeOfDay? reminderTime,
}) {
return AppSettings(
themeMode: themeMode ?? this.themeMode,
textSize: textSize ?? this.textSize,
highContrast: highContrast ?? this.highContrast,
reminderTime: reminderTime ?? this.reminderTime,
);
}
}
class SettingsNotifier extends Notifier<AppSettings> {
static const _keyThemeMode = 'settings.themeMode';
static const _keyTextSize = 'settings.textSize';
static const _keyHighContrast = 'settings.highContrast';
static const _keyReminderTime = 'settings.reminderTime';
SharedPreferences get _prefs => ref.read(sharedPreferencesProvider);
@override
AppSettings build() {
final reminderRaw = _prefs.getString(_keyReminderTime) ?? '09:00';
final parts = reminderRaw.split(':');
return AppSettings(
themeMode: ThemeMode.values.asNameMap()[_prefs.getString(_keyThemeMode)] ?? ThemeMode.system,
textSize: AppTextSize.values.asNameMap()[_prefs.getString(_keyTextSize)] ?? AppTextSize.normal,
highContrast: _prefs.getBool(_keyHighContrast) ?? false,
reminderTime: TimeOfDay(
hour: int.tryParse(parts[0]) ?? 9,
minute: int.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0,
),
);
}
void setThemeMode(ThemeMode mode) {
state = state.copyWith(themeMode: mode);
_prefs.setString(_keyThemeMode, mode.name);
}
void setTextSize(AppTextSize size) {
state = state.copyWith(textSize: size);
_prefs.setString(_keyTextSize, size.name);
}
void setHighContrast(bool enabled) {
state = state.copyWith(highContrast: enabled);
_prefs.setBool(_keyHighContrast, enabled);
}
void setReminderTime(TimeOfDay time) {
state = state.copyWith(reminderTime: time);
_prefs.setString(
_keyReminderTime,
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}',
);
}
}
final settingsProvider =
NotifierProvider<SettingsNotifier, AppSettings>(SettingsNotifier.new);

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
/// Planty-Grün als Ausgangsfarbe für das gesamte Farbschema.
const _seedColor = Color(0xFF2E7D32);
ThemeData buildAppTheme({
required Brightness brightness,
required bool highContrast,
}) {
final colorScheme = ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: brightness,
// Material 3 kann kontrastverstärkte Varianten desselben Schemas erzeugen.
contrastLevel: highContrast ? 1.0 : 0.0,
);
return ThemeData(
colorScheme: colorScheme,
// Großzügige Touch-Ziele: wichtig für die Bedienbarkeit durch ältere Nutzer.
materialTapTargetSize: MaterialTapTargetSize.padded,
visualDensity: VisualDensity.standard,
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(64, 52),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
listTileTheme: const ListTileThemeData(
minVerticalPadding: 12,
),
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
);
}

View file

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/today/application/due_tasks_provider.dart';
import '../../l10n/generated/app_localizations.dart';
import '../router/app_router.dart';
/// Das Hamburger-Menü: von jedem Haupt-Screen aus erreichbar,
/// listet alle Funktionen klassisch untereinander auf.
class AppDrawer extends ConsumerWidget {
const AppDrawer({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final currentPath = GoRouterState.of(context).uri.path;
final openTaskCount = ref.watch(dueTasksProvider).length;
void goTo(String path) {
Navigator.pop(context);
if (path != currentPath) {
context.go(path);
}
}
return Drawer(
child: SafeArea(
child: ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Icon(
Icons.eco,
size: 36,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Text(
l10n.appTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
),
const Divider(),
_DrawerItem(
icon: Icons.checklist,
label: l10n.menuToday,
selected: currentPath == AppRoutes.today,
badgeCount: openTaskCount,
onTap: () => goTo(AppRoutes.today),
),
_DrawerItem(
icon: Icons.local_florist,
label: l10n.menuPlants,
selected: currentPath == AppRoutes.plants,
onTap: () => goTo(AppRoutes.plants),
),
_DrawerItem(
icon: Icons.place,
label: l10n.menuLocations,
selected: currentPath == AppRoutes.locations,
onTap: () => goTo(AppRoutes.locations),
),
_DrawerItem(
icon: Icons.group,
label: l10n.menuHousehold,
selected: currentPath == AppRoutes.household,
onTap: () => goTo(AppRoutes.household),
),
_DrawerItem(
icon: Icons.settings,
label: l10n.menuSettings,
selected: currentPath == AppRoutes.settings,
onTap: () => goTo(AppRoutes.settings),
),
],
),
),
);
}
}
class _DrawerItem extends StatelessWidget {
const _DrawerItem({
required this.icon,
required this.label,
required this.selected,
required this.onTap,
this.badgeCount = 0,
});
final IconData icon;
final String label;
final bool selected;
final VoidCallback onTap;
final int badgeCount;
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(label, style: const TextStyle(fontSize: 17)),
selected: selected,
trailing: badgeCount > 0
? Badge.count(
count: badgeCount,
backgroundColor: Theme.of(context).colorScheme.primary,
)
: null,
onTap: onTap,
);
}
}

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import '../../../core/widgets/app_drawer.dart';
import '../../../l10n/generated/app_localizations.dart';
/// Platzhalter für das Haushalt-Sharing (V2): Einladungen, Mitglieder,
/// Sitter-Rolle.
class HouseholdScreen extends StatelessWidget {
const HouseholdScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.householdTitle)),
drawer: const AppDrawer(),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.group, size: 72, color: theme.colorScheme.primary),
const SizedBox(height: 16),
Text(
l10n.householdPlaceholder,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Chip(label: Text(l10n.householdComingSoon)),
],
),
),
),
);
}
}

View file

@ -0,0 +1,34 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../domain/plant_location.dart';
/// In-Memory-Stellplätze mit Demo-Daten wird später durch Firestore ersetzt.
class LocationsNotifier extends Notifier<List<PlantLocation>> {
@override
List<PlantLocation> build() {
return const [
PlantLocation(id: 'loc-wohnzimmer', name: 'Wohnzimmer'),
PlantLocation(id: 'loc-schlafzimmer', name: 'Schlafzimmer'),
PlantLocation(id: 'loc-kueche', name: 'Küche'),
];
}
void addLocation(String name) {
final id = 'loc-${DateTime.now().millisecondsSinceEpoch}';
state = [...state, PlantLocation(id: id, name: name)];
}
}
final locationsProvider =
NotifierProvider<LocationsNotifier, List<PlantLocation>>(
LocationsNotifier.new);
final locationByIdProvider =
Provider.family<PlantLocation?, String?>((ref, id) {
if (id == null) return null;
final locations = ref.watch(locationsProvider);
for (final location in locations) {
if (location.id == id) return location;
}
return null;
});

View file

@ -0,0 +1,13 @@
import 'package:flutter/foundation.dart';
/// Ein benannter Stellplatz im Haushalt, z. B. "Wohnzimmer-Fensterbank".
///
/// In V3 kommt hier die Standort-Analyse dazu (Foto, Licht-Einschätzung)
/// sowie eine Eignungs-Bewertung pro zugeordneter Pflanze.
@immutable
class PlantLocation {
const PlantLocation({required this.id, required this.name});
final String id;
final String name;
}

View file

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/widgets/app_drawer.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../plants/data/plants_provider.dart';
import '../data/locations_provider.dart';
class LocationsScreen extends ConsumerWidget {
const LocationsScreen({super.key});
Future<void> _addLocation(BuildContext context, WidgetRef ref) async {
final l10n = AppLocalizations.of(context);
final controller = TextEditingController();
final name = await showDialog<String>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(l10n.addLocation),
content: TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(
labelText: l10n.locationNameLabel,
border: const OutlineInputBorder(),
),
onSubmitted: (value) => Navigator.pop(dialogContext, value),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, controller.text),
child: Text(l10n.save),
),
],
),
);
if (name != null && name.trim().isNotEmpty) {
ref.read(locationsProvider.notifier).addLocation(name.trim());
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final locations = ref.watch(locationsProvider);
final plants = ref.watch(plantsProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.locationsTitle)),
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _addLocation(context, ref),
icon: const Icon(Icons.add),
label: Text(l10n.addLocation),
),
body: locations.isEmpty
? Center(
child: Text(
l10n.locationsEmpty,
style: Theme.of(context).textTheme.bodyLarge,
),
)
: ListView.separated(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 96),
itemCount: locations.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final location = locations[index];
final plantCount = plants
.where((plant) => plant.locationId == location.id)
.length;
return ListTile(
leading: const Icon(Icons.place),
title: Text(
location.name,
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Text(l10n.locationPlantCount(plantCount)),
);
},
),
);
}
}

View file

@ -0,0 +1,102 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../today/domain/due_task.dart';
import '../domain/plant.dart';
/// In-Memory-Datenhaltung mit Demo-Daten.
///
/// Wird im Firebase-Block durch ein Firestore-Repository ersetzt
/// die Provider-Schnittstelle nach außen bleibt dabei gleich.
class PlantsNotifier extends Notifier<List<Plant>> {
@override
List<Plant> build() {
final today = DateTime.now();
DateTime daysAgo(int days) =>
DateTime(today.year, today.month, today.day - days);
return [
Plant(
id: 'demo-1',
nickname: 'Monstera',
species: 'Monstera deliciosa (Fensterblatt)',
locationId: 'loc-wohnzimmer',
description:
'Beliebte Zimmerpflanze mit großen, geschlitzten Blättern. Mag helle Standorte ohne direkte Mittagssonne.',
careNotes:
'Erde zwischen den Wassergaben leicht antrocknen lassen. Staunässe vermeiden.',
wateringIntervalDays: 7,
fertilizingIntervalDays: 28,
lastWatered: daysAgo(8),
lastFertilized: daysAgo(14),
),
Plant(
id: 'demo-2',
nickname: 'Bogenhanf',
species: 'Sansevieria trifasciata',
locationId: 'loc-schlafzimmer',
description:
'Sehr pflegeleichte Pflanze mit aufrechten, festen Blättern. Verzeiht auch mal vergessenes Gießen.',
careNotes: 'Nur sparsam gießen, verträgt Trockenheit gut.',
wateringIntervalDays: 14,
fertilizingIntervalDays: 42,
lastWatered: daysAgo(14),
lastFertilized: daysAgo(20),
),
Plant(
id: 'demo-3',
nickname: 'Orchidee',
species: 'Phalaenopsis (Schmetterlingsorchidee)',
locationId: 'loc-kueche',
description:
'Klassische Fensterbank-Orchidee mit langen Blütenrispen. Mag helle, warme Plätze.',
careNotes: 'Einmal pro Woche tauchen statt gießen. Kein Wasser im Herz der Pflanze stehen lassen.',
wateringIntervalDays: 7,
fertilizingIntervalDays: 21,
lastWatered: daysAgo(3),
lastFertilized: daysAgo(10),
),
];
}
void addPlant(Plant plant) {
state = [...state, plant];
}
void updatePlant(Plant updated) {
state = [
for (final plant in state)
if (plant.id == updated.id) updated else plant,
];
}
void removePlant(String plantId) {
state = state.where((plant) => plant.id != plantId).toList();
}
/// Bestätigt eine Aufgabe: setzt das Erledigungsdatum, wodurch sich die
/// nächste Fälligkeit automatisch neu berechnet (Intervall-Modell).
void confirmTask(String plantId, CareTaskType type, {DateTime? when}) {
final now = when ?? DateTime.now();
state = [
for (final plant in state)
if (plant.id == plantId)
switch (type) {
CareTaskType.watering => plant.copyWith(lastWatered: now),
CareTaskType.fertilizing => plant.copyWith(lastFertilized: now),
}
else
plant,
];
}
}
final plantsProvider =
NotifierProvider<PlantsNotifier, List<Plant>>(PlantsNotifier.new);
final plantByIdProvider = Provider.family<Plant?, String>((ref, id) {
final plants = ref.watch(plantsProvider);
for (final plant in plants) {
if (plant.id == id) return plant;
}
return null;
});

View file

@ -0,0 +1,71 @@
import 'package:flutter/foundation.dart';
/// Eine Pflanze im Haushalt.
///
/// Das Modell ist bewusst haushaltszentriert gedacht: Sobald das
/// Firebase-Backend angebunden ist, leben Pflanzen unter
/// `households/{householdId}/plants/{plantId}`.
@immutable
class Plant {
const Plant({
required this.id,
required this.nickname,
required this.species,
required this.wateringIntervalDays,
required this.fertilizingIntervalDays,
this.locationId,
this.description = '',
this.careNotes = '',
this.lastWatered,
this.lastFertilized,
});
final String id;
/// Vom Nutzer vergebener Name, z. B. "Omas Monstera".
final String nickname;
/// Pflanzenart (deutsch und/oder botanisch). Wird später von der
/// PlantNet-Erkennung befüllt, kann aber auch frei eingegeben werden.
final String species;
/// Referenz auf einen Stellplatz des Haushalts (optional).
final String? locationId;
/// Beschreibung und Pflegehinweise werden später von Claude erzeugt.
final String description;
final String careNotes;
final int wateringIntervalDays;
final int fertilizingIntervalDays;
/// `null` bedeutet: noch nie erledigt Aufgabe ist sofort fällig.
final DateTime? lastWatered;
final DateTime? lastFertilized;
Plant copyWith({
String? nickname,
String? species,
String? Function()? locationId,
String? description,
String? careNotes,
int? wateringIntervalDays,
int? fertilizingIntervalDays,
DateTime? lastWatered,
DateTime? lastFertilized,
}) {
return Plant(
id: id,
nickname: nickname ?? this.nickname,
species: species ?? this.species,
locationId: locationId != null ? locationId() : this.locationId,
description: description ?? this.description,
careNotes: careNotes ?? this.careNotes,
wateringIntervalDays: wateringIntervalDays ?? this.wateringIntervalDays,
fertilizingIntervalDays:
fertilizingIntervalDays ?? this.fertilizingIntervalDays,
lastWatered: lastWatered ?? this.lastWatered,
lastFertilized: lastFertilized ?? this.lastFertilized,
);
}
}

View file

@ -0,0 +1,170 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../core/router/app_router.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../locations/data/locations_provider.dart';
import '../data/plants_provider.dart';
class PlantDetailScreen extends ConsumerWidget {
const PlantDetailScreen({super.key, required this.plantId});
final String plantId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final plant = ref.watch(plantByIdProvider(plantId));
if (plant == null) {
// Pflanze wurde gelöscht, während der Screen offen war.
return Scaffold(appBar: AppBar(), body: const SizedBox.shrink());
}
final location = ref.watch(locationByIdProvider(plant.locationId));
final locale = Localizations.localeOf(context).toString();
final dateFormat = DateFormat('d. MMMM y', locale);
String formatDate(DateTime? date) =>
date != null ? dateFormat.format(date) : l10n.never;
return Scaffold(
appBar: AppBar(
title: Text(plant.nickname),
actions: [
IconButton(
icon: const Icon(Icons.edit),
tooltip: l10n.plantDetailTitle,
onPressed: () => context.push(AppRoutes.plantEdit, extra: plant),
),
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: l10n.delete,
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(l10n.deletePlantQuestion(plant.nickname)),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: Text(l10n.delete),
),
],
),
);
if (confirmed == true && context.mounted) {
ref.read(plantsProvider.notifier).removePlant(plant.id);
context.pop();
}
},
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_InfoTile(
icon: Icons.local_florist,
label: l10n.speciesLabel,
value: plant.species,
),
_InfoTile(
icon: Icons.place,
label: l10n.locationLabel,
value: location?.name ?? l10n.locationNone,
),
const Divider(height: 32),
_InfoTile(
icon: Icons.water_drop,
label: l10n.wateringEvery(plant.wateringIntervalDays),
value: l10n.lastWatered(formatDate(plant.lastWatered)),
),
_InfoTile(
icon: Icons.compost,
label: l10n.fertilizingEvery(plant.fertilizingIntervalDays),
value: l10n.lastFertilized(formatDate(plant.lastFertilized)),
),
if (plant.description.isNotEmpty) ...[
const Divider(height: 32),
Text(
plant.description,
style: Theme.of(context).textTheme.bodyLarge,
),
],
if (plant.careNotes.isNotEmpty) ...[
const SizedBox(height: 12),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.tips_and_updates,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Text(
plant.careNotes,
style: Theme.of(context).textTheme.bodyLarge,
),
),
],
),
),
),
],
],
),
);
}
}
class _InfoTile extends StatelessWidget {
const _InfoTile({
required this.icon,
required this.label,
required this.value,
});
final IconData icon;
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: theme.colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: theme.textTheme.titleMedium),
const SizedBox(height: 2),
Text(
value,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../locations/data/locations_provider.dart';
import '../data/plants_provider.dart';
import '../domain/plant.dart';
/// Manuelles Anlegen und Bearbeiten einer Pflanze.
///
/// Im KI-Block von V1 wird dieser Screen um den Foto-Flow ergänzt:
/// Foto PlantNet-Erkennung Claude befüllt Art, Beschreibung und
/// Intervall-Vorschläge vor der Nutzer bestätigt hier nur noch.
class PlantFormScreen extends ConsumerStatefulWidget {
const PlantFormScreen({super.key, this.existing});
final Plant? existing;
@override
ConsumerState<PlantFormScreen> createState() => _PlantFormScreenState();
}
class _PlantFormScreenState extends ConsumerState<PlantFormScreen> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nicknameController;
late final TextEditingController _speciesController;
late final TextEditingController _wateringController;
late final TextEditingController _fertilizingController;
String? _locationId;
@override
void initState() {
super.initState();
final existing = widget.existing;
_nicknameController = TextEditingController(text: existing?.nickname ?? '');
_speciesController = TextEditingController(text: existing?.species ?? '');
_wateringController = TextEditingController(
text: existing?.wateringIntervalDays.toString() ?? '7');
_fertilizingController = TextEditingController(
text: existing?.fertilizingIntervalDays.toString() ?? '28');
_locationId = existing?.locationId;
}
@override
void dispose() {
_nicknameController.dispose();
_speciesController.dispose();
_wateringController.dispose();
_fertilizingController.dispose();
super.dispose();
}
void _save() {
if (!_formKey.currentState!.validate()) return;
final notifier = ref.read(plantsProvider.notifier);
final existing = widget.existing;
if (existing != null) {
notifier.updatePlant(existing.copyWith(
nickname: _nicknameController.text.trim(),
species: _speciesController.text.trim(),
locationId: () => _locationId,
wateringIntervalDays: int.parse(_wateringController.text.trim()),
fertilizingIntervalDays: int.parse(_fertilizingController.text.trim()),
));
} else {
notifier.addPlant(Plant(
id: 'plant-${DateTime.now().millisecondsSinceEpoch}',
nickname: _nicknameController.text.trim(),
species: _speciesController.text.trim(),
locationId: _locationId,
wateringIntervalDays: int.parse(_wateringController.text.trim()),
fertilizingIntervalDays: int.parse(_fertilizingController.text.trim()),
));
}
context.pop();
}
String? _validateRequired(String? value) {
final l10n = AppLocalizations.of(context);
if (value == null || value.trim().isEmpty) return l10n.requiredField;
return null;
}
String? _validateInterval(String? value) {
final l10n = AppLocalizations.of(context);
final parsed = int.tryParse(value?.trim() ?? '');
if (parsed == null || parsed <= 0) return l10n.invalidNumber;
return null;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final locations = ref.watch(locationsProvider);
return Scaffold(
appBar: AppBar(
title: Text(
widget.existing != null ? l10n.plantDetailTitle : l10n.addPlant),
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
controller: _nicknameController,
decoration: InputDecoration(
labelText: l10n.nicknameLabel,
border: const OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
validator: _validateRequired,
),
const SizedBox(height: 16),
TextFormField(
controller: _speciesController,
decoration: InputDecoration(
labelText: l10n.speciesLabel,
border: const OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
validator: _validateRequired,
),
const SizedBox(height: 16),
DropdownButtonFormField<String?>(
initialValue: _locationId,
decoration: InputDecoration(
labelText: l10n.locationLabel,
border: const OutlineInputBorder(),
),
items: [
DropdownMenuItem<String?>(
value: null,
child: Text(l10n.locationNone),
),
for (final location in locations)
DropdownMenuItem<String?>(
value: location.id,
child: Text(location.name),
),
],
onChanged: (value) => setState(() => _locationId = value),
),
const SizedBox(height: 16),
TextFormField(
controller: _wateringController,
decoration: InputDecoration(
labelText: l10n.wateringIntervalLabel,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.water_drop),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: _validateInterval,
),
const SizedBox(height: 16),
TextFormField(
controller: _fertilizingController,
decoration: InputDecoration(
labelText: l10n.fertilizingIntervalLabel,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.compost),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
validator: _validateInterval,
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _save,
icon: const Icon(Icons.check),
label: Text(l10n.save),
),
const SizedBox(height: 8),
TextButton(
onPressed: () => context.pop(),
child: Text(l10n.cancel),
),
],
),
),
);
}
}

View file

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/router/app_router.dart';
import '../../../core/widgets/app_drawer.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../locations/data/locations_provider.dart';
import '../data/plants_provider.dart';
class PlantsScreen extends ConsumerWidget {
const PlantsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final plants = ref.watch(plantsProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.plantsTitle)),
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => context.push(AppRoutes.plantAdd),
icon: const Icon(Icons.add),
label: Text(l10n.addPlant),
),
body: plants.isEmpty
? _EmptyView(l10n: l10n)
: ListView.separated(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 96),
itemCount: plants.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final plant = plants[index];
final location =
ref.watch(locationByIdProvider(plant.locationId));
return ListTile(
leading: CircleAvatar(
radius: 24,
child: const Icon(Icons.local_florist),
),
title: Text(
plant.nickname,
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Text(
location != null
? '${plant.species}\n${location.name}'
: plant.species,
),
isThreeLine: location != null,
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push('/plants/${plant.id}'),
);
},
),
);
}
}
class _EmptyView extends StatelessWidget {
const _EmptyView({required this.l10n});
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.local_florist, size: 72),
const SizedBox(height: 16),
Text(
l10n.plantsEmpty,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.plantsEmptyHint,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
),
);
}
}

View file

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/settings/settings_provider.dart';
import '../../../core/widgets/app_drawer.dart';
import '../../../l10n/generated/app_localizations.dart';
class SettingsScreen extends ConsumerWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final settings = ref.watch(settingsProvider);
final notifier = ref.read(settingsProvider.notifier);
return Scaffold(
appBar: AppBar(title: Text(l10n.settingsTitle)),
drawer: const AppDrawer(),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_SectionTitle(l10n.settingsAppearance),
SegmentedButton<ThemeMode>(
segments: [
ButtonSegment(
value: ThemeMode.system,
label: Text(l10n.themeSystem),
icon: const Icon(Icons.brightness_auto),
),
ButtonSegment(
value: ThemeMode.light,
label: Text(l10n.themeLight),
icon: const Icon(Icons.light_mode),
),
ButtonSegment(
value: ThemeMode.dark,
label: Text(l10n.themeDark),
icon: const Icon(Icons.dark_mode),
),
],
selected: {settings.themeMode},
onSelectionChanged: (selection) =>
notifier.setThemeMode(selection.first),
),
const SizedBox(height: 24),
_SectionTitle(l10n.settingsTextSize),
SegmentedButton<AppTextSize>(
segments: [
ButtonSegment(
value: AppTextSize.normal,
label: Text(l10n.textSizeNormal),
),
ButtonSegment(
value: AppTextSize.large,
label: Text(l10n.textSizeLarge),
),
ButtonSegment(
value: AppTextSize.extraLarge,
label: Text(l10n.textSizeXLarge),
),
],
selected: {settings.textSize},
onSelectionChanged: (selection) =>
notifier.setTextSize(selection.first),
),
const SizedBox(height: 16),
SwitchListTile(
title: Text(l10n.settingsHighContrast),
value: settings.highContrast,
onChanged: notifier.setHighContrast,
contentPadding: EdgeInsets.zero,
),
const Divider(height: 32),
_SectionTitle(l10n.settingsReminderTime),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.notifications_active),
title: Text(
settings.reminderTime.format(context),
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(l10n.settingsReminderTimeHint),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final picked = await showTimePicker(
context: context,
initialTime: settings.reminderTime,
);
if (picked != null) {
notifier.setReminderTime(picked);
}
},
),
],
),
);
}
}
class _SectionTitle extends StatelessWidget {
const _SectionTitle(this.title);
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
);
}
}

View file

@ -0,0 +1,61 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../plants/data/plants_provider.dart';
import '../../plants/domain/plant.dart';
import '../domain/due_task.dart';
DateTime _dateOnly(DateTime dt) => DateTime(dt.year, dt.month, dt.day);
DateTime? _nextDue(DateTime? lastDone, int intervalDays, DateTime today) {
// Noch nie erledigt sofort fällig.
if (lastDone == null) return today;
return _dateOnly(lastDone).add(Duration(days: intervalDays));
}
Iterable<DueTask> _tasksForPlant(Plant plant, DateTime today) sync* {
final watering = _nextDue(plant.lastWatered, plant.wateringIntervalDays, today);
if (watering != null && !watering.isAfter(today)) {
yield DueTask(plant: plant, type: CareTaskType.watering, dueDate: watering);
}
final fertilizing =
_nextDue(plant.lastFertilized, plant.fertilizingIntervalDays, today);
if (fertilizing != null && !fertilizing.isAfter(today)) {
yield DueTask(
plant: plant, type: CareTaskType.fertilizing, dueDate: fertilizing);
}
}
/// Alle heute fälligen und überfälligen Aufgaben, Überfälliges zuerst.
final dueTasksProvider = Provider<List<DueTask>>((ref) {
final plants = ref.watch(plantsProvider);
final today = _dateOnly(DateTime.now());
final tasks = [
for (final plant in plants) ..._tasksForPlant(plant, today),
];
tasks.sort((a, b) {
final byDate = a.dueDate.compareTo(b.dueDate);
if (byDate != 0) return byDate;
return a.plant.nickname.compareTo(b.plant.nickname);
});
return tasks;
});
/// Das nächste zukünftige Fälligkeitsdatum für den "Alles versorgt"-Screen
/// ("Nächste Aufgabe: Freitag").
final nextDueDateProvider = Provider<DateTime?>((ref) {
final plants = ref.watch(plantsProvider);
final today = _dateOnly(DateTime.now());
DateTime? next;
for (final plant in plants) {
for (final candidate in [
_nextDue(plant.lastWatered, plant.wateringIntervalDays, today),
_nextDue(plant.lastFertilized, plant.fertilizingIntervalDays, today),
]) {
if (candidate == null || !candidate.isAfter(today)) continue;
if (next == null || candidate.isBefore(next)) next = candidate;
}
}
return next;
});

View file

@ -0,0 +1,25 @@
import 'package:flutter/foundation.dart';
import '../../plants/domain/plant.dart';
enum CareTaskType { watering, fertilizing }
/// Eine heute fällige (oder überfällige) Pflegeaufgabe.
///
/// Aufgaben werden nicht gespeichert, sondern aus den Pflanzen berechnet:
/// Fälligkeit = letzte Erledigung + Intervall (Intervall-Modell).
@immutable
class DueTask {
const DueTask({
required this.plant,
required this.type,
required this.dueDate,
});
final Plant plant;
final CareTaskType type;
final DateTime dueDate;
/// 0 = heute fällig, 1 = seit gestern überfällig, usw.
int overdueDays(DateTime today) => today.difference(dueDate).inDays;
}

View file

@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../core/router/app_router.dart';
import '../../../core/widgets/app_drawer.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../plants/data/plants_provider.dart';
import '../application/due_tasks_provider.dart';
import '../domain/due_task.dart';
/// Start-Screen der App: die Tages-Checkliste.
/// Push-Erinnerungen führen später direkt hierher.
class TodayScreen extends ConsumerWidget {
const TodayScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final tasks = ref.watch(dueTasksProvider);
return Scaffold(
appBar: AppBar(title: Text(l10n.todayTitle)),
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => context.push(AppRoutes.plantAdd),
icon: const Icon(Icons.add),
label: Text(l10n.addPlant),
),
body: tasks.isEmpty ? const _AllDoneView() : _TaskListView(tasks: tasks),
);
}
}
class _TaskListView extends ConsumerWidget {
const _TaskListView({required this.tasks});
final List<DueTask> tasks;
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
return ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 96),
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
l10n.todayOpenTasks(tasks.length),
style: Theme.of(context).textTheme.titleMedium,
),
),
for (final task in tasks) _TaskCard(task: task),
],
);
}
}
class _TaskCard extends ConsumerWidget {
const _TaskCard({required this.task});
final DueTask task;
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
final today = DateTime.now();
final overdueDays =
task.overdueDays(DateTime(today.year, today.month, today.day));
final title = switch (task.type) {
CareTaskType.watering => l10n.taskWaterTitle(task.plant.nickname),
CareTaskType.fertilizing => l10n.taskFertilizeTitle(task.plant.nickname),
};
final icon = switch (task.type) {
CareTaskType.watering => Icons.water_drop,
CareTaskType.fertilizing => Icons.compost,
};
return Card(
margin: const EdgeInsets.symmetric(vertical: 6),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(icon, size: 32, color: theme.colorScheme.primary),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleMedium),
const SizedBox(height: 4),
Text(
overdueDays > 0
? l10n.taskOverdue(overdueDays)
: l10n.taskDueToday,
style: theme.textTheme.bodyMedium?.copyWith(
color: overdueDays > 0
? theme.colorScheme.error
: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 12),
FilledButton.icon(
onPressed: () {
ref
.read(plantsProvider.notifier)
.confirmTask(task.plant.id, task.type);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content:
Text(l10n.taskDoneConfirmation(task.plant.nickname)),
),
);
},
icon: const Icon(Icons.check),
label: Text(l10n.taskDone),
),
],
),
),
);
}
}
class _AllDoneView extends ConsumerWidget {
const _AllDoneView();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
final nextDue = ref.watch(nextDueDateProvider);
final locale = Localizations.localeOf(context).toString();
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle,
size: 72,
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
l10n.todayAllDone,
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
nextDue != null
? l10n.todayNextTask(
DateFormat('EEEE, d. MMMM', locale).format(nextDue))
: l10n.todayEmptyHint,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
),
);
}
}

123
lib/l10n/app_de.arb Normal file
View file

@ -0,0 +1,123 @@
{
"@@locale": "de",
"appTitle": "Planty",
"menuToday": "Heute",
"menuPlants": "Meine Pflanzen",
"menuLocations": "Stellplätze",
"menuHousehold": "Haushalt",
"menuSettings": "Einstellungen",
"todayTitle": "Heute",
"todayAllDone": "Alles versorgt!",
"todayEmptyHint": "Heute stehen keine Aufgaben an.",
"todayNextTask": "Nächste Aufgabe: {date}",
"@todayNextTask": {
"placeholders": {
"date": { "type": "String" }
}
},
"todayOpenTasks": "{count, plural, one{1 offene Aufgabe} other{{count} offene Aufgaben}}",
"@todayOpenTasks": {
"placeholders": {
"count": { "type": "int" }
}
},
"taskWaterTitle": "{name} gießen",
"@taskWaterTitle": {
"placeholders": {
"name": { "type": "String" }
}
},
"taskFertilizeTitle": "{name} düngen",
"@taskFertilizeTitle": {
"placeholders": {
"name": { "type": "String" }
}
},
"taskDueToday": "Heute fällig",
"taskOverdue": "{days, plural, one{Seit gestern überfällig} other{Seit {days} Tagen überfällig}}",
"@taskOverdue": {
"placeholders": {
"days": { "type": "int" }
}
},
"taskDone": "Erledigt",
"taskDoneConfirmation": "{name} als erledigt markiert.",
"@taskDoneConfirmation": {
"placeholders": {
"name": { "type": "String" }
}
},
"addPlant": "Pflanze hinzufügen",
"plantsTitle": "Meine Pflanzen",
"plantsEmpty": "Noch keine Pflanzen angelegt.",
"plantsEmptyHint": "Lege deine erste Pflanze über den Button unten an.",
"plantDetailTitle": "Pflanzenprofil",
"nicknameLabel": "Spitzname",
"speciesLabel": "Pflanzenart",
"locationLabel": "Stellplatz",
"locationNone": "Kein Stellplatz",
"wateringEvery": "Gießen: alle {days} Tage",
"@wateringEvery": {
"placeholders": {
"days": { "type": "int" }
}
},
"fertilizingEvery": "Düngen: alle {days} Tage",
"@fertilizingEvery": {
"placeholders": {
"days": { "type": "int" }
}
},
"wateringIntervalLabel": "Gieß-Intervall (Tage)",
"fertilizingIntervalLabel": "Dünge-Intervall (Tage)",
"lastWatered": "Zuletzt gegossen: {date}",
"@lastWatered": {
"placeholders": {
"date": { "type": "String" }
}
},
"lastFertilized": "Zuletzt gedüngt: {date}",
"@lastFertilized": {
"placeholders": {
"date": { "type": "String" }
}
},
"never": "noch nie",
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"deletePlantQuestion": "{name} wirklich löschen?",
"@deletePlantQuestion": {
"placeholders": {
"name": { "type": "String" }
}
},
"requiredField": "Bitte ausfüllen",
"invalidNumber": "Bitte eine Zahl größer 0 eingeben",
"locationsTitle": "Stellplätze",
"locationsEmpty": "Noch keine Stellplätze angelegt.",
"addLocation": "Stellplatz hinzufügen",
"locationNameLabel": "Name des Stellplatzes",
"locationPlantCount": "{count, plural, =0{Keine Pflanzen} one{1 Pflanze} other{{count} Pflanzen}}",
"@locationPlantCount": {
"placeholders": {
"count": { "type": "int" }
}
},
"householdTitle": "Haushalt",
"householdPlaceholder": "Hier kannst du bald deinen Haushalt teilen, damit Familie oder Pflanzen-Sitter die Pflanzen mitversorgen können.",
"householdComingSoon": "Kommt in Version 2",
"settingsTitle": "Einstellungen",
"settingsAppearance": "Erscheinungsbild",
"themeSystem": "System",
"themeLight": "Hell",
"themeDark": "Dunkel",
"settingsTextSize": "Schriftgröße",
"textSizeNormal": "Normal",
"textSizeLarge": "Groß",
"textSizeXLarge": "Sehr groß",
"settingsHighContrast": "Hoher Kontrast",
"settingsReminderTime": "Erinnerungszeit",
"settingsReminderTimeHint": "Zu dieser Uhrzeit erinnert dich Planty an offene Aufgaben.",
"demoDataBanner": "Demo-Daten werden durch deine echten Pflanzen ersetzt"
}

View file

@ -0,0 +1,488 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_de.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'generated/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[Locale('de')];
/// No description provided for @appTitle.
///
/// In de, this message translates to:
/// **'Planty'**
String get appTitle;
/// No description provided for @menuToday.
///
/// In de, this message translates to:
/// **'Heute'**
String get menuToday;
/// No description provided for @menuPlants.
///
/// In de, this message translates to:
/// **'Meine Pflanzen'**
String get menuPlants;
/// No description provided for @menuLocations.
///
/// In de, this message translates to:
/// **'Stellplätze'**
String get menuLocations;
/// No description provided for @menuHousehold.
///
/// In de, this message translates to:
/// **'Haushalt'**
String get menuHousehold;
/// No description provided for @menuSettings.
///
/// In de, this message translates to:
/// **'Einstellungen'**
String get menuSettings;
/// No description provided for @todayTitle.
///
/// In de, this message translates to:
/// **'Heute'**
String get todayTitle;
/// No description provided for @todayAllDone.
///
/// In de, this message translates to:
/// **'Alles versorgt!'**
String get todayAllDone;
/// No description provided for @todayEmptyHint.
///
/// In de, this message translates to:
/// **'Heute stehen keine Aufgaben an.'**
String get todayEmptyHint;
/// No description provided for @todayNextTask.
///
/// In de, this message translates to:
/// **'Nächste Aufgabe: {date}'**
String todayNextTask(String date);
/// No description provided for @todayOpenTasks.
///
/// In de, this message translates to:
/// **'{count, plural, one{1 offene Aufgabe} other{{count} offene Aufgaben}}'**
String todayOpenTasks(int count);
/// No description provided for @taskWaterTitle.
///
/// In de, this message translates to:
/// **'{name} gießen'**
String taskWaterTitle(String name);
/// No description provided for @taskFertilizeTitle.
///
/// In de, this message translates to:
/// **'{name} düngen'**
String taskFertilizeTitle(String name);
/// No description provided for @taskDueToday.
///
/// In de, this message translates to:
/// **'Heute fällig'**
String get taskDueToday;
/// No description provided for @taskOverdue.
///
/// In de, this message translates to:
/// **'{days, plural, one{Seit gestern überfällig} other{Seit {days} Tagen überfällig}}'**
String taskOverdue(int days);
/// No description provided for @taskDone.
///
/// In de, this message translates to:
/// **'Erledigt'**
String get taskDone;
/// No description provided for @taskDoneConfirmation.
///
/// In de, this message translates to:
/// **'{name} als erledigt markiert.'**
String taskDoneConfirmation(String name);
/// No description provided for @addPlant.
///
/// In de, this message translates to:
/// **'Pflanze hinzufügen'**
String get addPlant;
/// No description provided for @plantsTitle.
///
/// In de, this message translates to:
/// **'Meine Pflanzen'**
String get plantsTitle;
/// No description provided for @plantsEmpty.
///
/// In de, this message translates to:
/// **'Noch keine Pflanzen angelegt.'**
String get plantsEmpty;
/// No description provided for @plantsEmptyHint.
///
/// In de, this message translates to:
/// **'Lege deine erste Pflanze über den Button unten an.'**
String get plantsEmptyHint;
/// No description provided for @plantDetailTitle.
///
/// In de, this message translates to:
/// **'Pflanzenprofil'**
String get plantDetailTitle;
/// No description provided for @nicknameLabel.
///
/// In de, this message translates to:
/// **'Spitzname'**
String get nicknameLabel;
/// No description provided for @speciesLabel.
///
/// In de, this message translates to:
/// **'Pflanzenart'**
String get speciesLabel;
/// No description provided for @locationLabel.
///
/// In de, this message translates to:
/// **'Stellplatz'**
String get locationLabel;
/// No description provided for @locationNone.
///
/// In de, this message translates to:
/// **'Kein Stellplatz'**
String get locationNone;
/// No description provided for @wateringEvery.
///
/// In de, this message translates to:
/// **'Gießen: alle {days} Tage'**
String wateringEvery(int days);
/// No description provided for @fertilizingEvery.
///
/// In de, this message translates to:
/// **'Düngen: alle {days} Tage'**
String fertilizingEvery(int days);
/// No description provided for @wateringIntervalLabel.
///
/// In de, this message translates to:
/// **'Gieß-Intervall (Tage)'**
String get wateringIntervalLabel;
/// No description provided for @fertilizingIntervalLabel.
///
/// In de, this message translates to:
/// **'Dünge-Intervall (Tage)'**
String get fertilizingIntervalLabel;
/// No description provided for @lastWatered.
///
/// In de, this message translates to:
/// **'Zuletzt gegossen: {date}'**
String lastWatered(String date);
/// No description provided for @lastFertilized.
///
/// In de, this message translates to:
/// **'Zuletzt gedüngt: {date}'**
String lastFertilized(String date);
/// No description provided for @never.
///
/// In de, this message translates to:
/// **'noch nie'**
String get never;
/// No description provided for @save.
///
/// In de, this message translates to:
/// **'Speichern'**
String get save;
/// No description provided for @cancel.
///
/// In de, this message translates to:
/// **'Abbrechen'**
String get cancel;
/// No description provided for @delete.
///
/// In de, this message translates to:
/// **'Löschen'**
String get delete;
/// No description provided for @deletePlantQuestion.
///
/// In de, this message translates to:
/// **'{name} wirklich löschen?'**
String deletePlantQuestion(String name);
/// No description provided for @requiredField.
///
/// In de, this message translates to:
/// **'Bitte ausfüllen'**
String get requiredField;
/// No description provided for @invalidNumber.
///
/// In de, this message translates to:
/// **'Bitte eine Zahl größer 0 eingeben'**
String get invalidNumber;
/// No description provided for @locationsTitle.
///
/// In de, this message translates to:
/// **'Stellplätze'**
String get locationsTitle;
/// No description provided for @locationsEmpty.
///
/// In de, this message translates to:
/// **'Noch keine Stellplätze angelegt.'**
String get locationsEmpty;
/// No description provided for @addLocation.
///
/// In de, this message translates to:
/// **'Stellplatz hinzufügen'**
String get addLocation;
/// No description provided for @locationNameLabel.
///
/// In de, this message translates to:
/// **'Name des Stellplatzes'**
String get locationNameLabel;
/// No description provided for @locationPlantCount.
///
/// In de, this message translates to:
/// **'{count, plural, =0{Keine Pflanzen} one{1 Pflanze} other{{count} Pflanzen}}'**
String locationPlantCount(int count);
/// No description provided for @householdTitle.
///
/// In de, this message translates to:
/// **'Haushalt'**
String get householdTitle;
/// No description provided for @householdPlaceholder.
///
/// In de, this message translates to:
/// **'Hier kannst du bald deinen Haushalt teilen, damit Familie oder Pflanzen-Sitter die Pflanzen mitversorgen können.'**
String get householdPlaceholder;
/// No description provided for @householdComingSoon.
///
/// In de, this message translates to:
/// **'Kommt in Version 2'**
String get householdComingSoon;
/// No description provided for @settingsTitle.
///
/// In de, this message translates to:
/// **'Einstellungen'**
String get settingsTitle;
/// No description provided for @settingsAppearance.
///
/// In de, this message translates to:
/// **'Erscheinungsbild'**
String get settingsAppearance;
/// No description provided for @themeSystem.
///
/// In de, this message translates to:
/// **'System'**
String get themeSystem;
/// No description provided for @themeLight.
///
/// In de, this message translates to:
/// **'Hell'**
String get themeLight;
/// No description provided for @themeDark.
///
/// In de, this message translates to:
/// **'Dunkel'**
String get themeDark;
/// No description provided for @settingsTextSize.
///
/// In de, this message translates to:
/// **'Schriftgröße'**
String get settingsTextSize;
/// No description provided for @textSizeNormal.
///
/// In de, this message translates to:
/// **'Normal'**
String get textSizeNormal;
/// No description provided for @textSizeLarge.
///
/// In de, this message translates to:
/// **'Groß'**
String get textSizeLarge;
/// No description provided for @textSizeXLarge.
///
/// In de, this message translates to:
/// **'Sehr groß'**
String get textSizeXLarge;
/// No description provided for @settingsHighContrast.
///
/// In de, this message translates to:
/// **'Hoher Kontrast'**
String get settingsHighContrast;
/// No description provided for @settingsReminderTime.
///
/// In de, this message translates to:
/// **'Erinnerungszeit'**
String get settingsReminderTime;
/// No description provided for @settingsReminderTimeHint.
///
/// In de, this message translates to:
/// **'Zu dieser Uhrzeit erinnert dich Planty an offene Aufgaben.'**
String get settingsReminderTimeHint;
/// No description provided for @demoDataBanner.
///
/// In de, this message translates to:
/// **'Demo-Daten werden durch deine echten Pflanzen ersetzt'**
String get demoDataBanner;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['de'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View file

@ -0,0 +1,237 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for German (`de`).
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get appTitle => 'Planty';
@override
String get menuToday => 'Heute';
@override
String get menuPlants => 'Meine Pflanzen';
@override
String get menuLocations => 'Stellplätze';
@override
String get menuHousehold => 'Haushalt';
@override
String get menuSettings => 'Einstellungen';
@override
String get todayTitle => 'Heute';
@override
String get todayAllDone => 'Alles versorgt!';
@override
String get todayEmptyHint => 'Heute stehen keine Aufgaben an.';
@override
String todayNextTask(String date) {
return 'Nächste Aufgabe: $date';
}
@override
String todayOpenTasks(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count offene Aufgaben',
one: '1 offene Aufgabe',
);
return '$_temp0';
}
@override
String taskWaterTitle(String name) {
return '$name gießen';
}
@override
String taskFertilizeTitle(String name) {
return '$name düngen';
}
@override
String get taskDueToday => 'Heute fällig';
@override
String taskOverdue(int days) {
String _temp0 = intl.Intl.pluralLogic(
days,
locale: localeName,
other: 'Seit $days Tagen überfällig',
one: 'Seit gestern überfällig',
);
return '$_temp0';
}
@override
String get taskDone => 'Erledigt';
@override
String taskDoneConfirmation(String name) {
return '$name als erledigt markiert.';
}
@override
String get addPlant => 'Pflanze hinzufügen';
@override
String get plantsTitle => 'Meine Pflanzen';
@override
String get plantsEmpty => 'Noch keine Pflanzen angelegt.';
@override
String get plantsEmptyHint =>
'Lege deine erste Pflanze über den Button unten an.';
@override
String get plantDetailTitle => 'Pflanzenprofil';
@override
String get nicknameLabel => 'Spitzname';
@override
String get speciesLabel => 'Pflanzenart';
@override
String get locationLabel => 'Stellplatz';
@override
String get locationNone => 'Kein Stellplatz';
@override
String wateringEvery(int days) {
return 'Gießen: alle $days Tage';
}
@override
String fertilizingEvery(int days) {
return 'Düngen: alle $days Tage';
}
@override
String get wateringIntervalLabel => 'Gieß-Intervall (Tage)';
@override
String get fertilizingIntervalLabel => 'Dünge-Intervall (Tage)';
@override
String lastWatered(String date) {
return 'Zuletzt gegossen: $date';
}
@override
String lastFertilized(String date) {
return 'Zuletzt gedüngt: $date';
}
@override
String get never => 'noch nie';
@override
String get save => 'Speichern';
@override
String get cancel => 'Abbrechen';
@override
String get delete => 'Löschen';
@override
String deletePlantQuestion(String name) {
return '$name wirklich löschen?';
}
@override
String get requiredField => 'Bitte ausfüllen';
@override
String get invalidNumber => 'Bitte eine Zahl größer 0 eingeben';
@override
String get locationsTitle => 'Stellplätze';
@override
String get locationsEmpty => 'Noch keine Stellplätze angelegt.';
@override
String get addLocation => 'Stellplatz hinzufügen';
@override
String get locationNameLabel => 'Name des Stellplatzes';
@override
String locationPlantCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Pflanzen',
one: '1 Pflanze',
zero: 'Keine Pflanzen',
);
return '$_temp0';
}
@override
String get householdTitle => 'Haushalt';
@override
String get householdPlaceholder =>
'Hier kannst du bald deinen Haushalt teilen, damit Familie oder Pflanzen-Sitter die Pflanzen mitversorgen können.';
@override
String get householdComingSoon => 'Kommt in Version 2';
@override
String get settingsTitle => 'Einstellungen';
@override
String get settingsAppearance => 'Erscheinungsbild';
@override
String get themeSystem => 'System';
@override
String get themeLight => 'Hell';
@override
String get themeDark => 'Dunkel';
@override
String get settingsTextSize => 'Schriftgröße';
@override
String get textSizeNormal => 'Normal';
@override
String get textSizeLarge => 'Groß';
@override
String get textSizeXLarge => 'Sehr groß';
@override
String get settingsHighContrast => 'Hoher Kontrast';
@override
String get settingsReminderTime => 'Erinnerungszeit';
@override
String get settingsReminderTimeHint =>
'Zu dieser Uhrzeit erinnert dich Planty an offene Aufgaben.';
@override
String get demoDataBanner =>
'Demo-Daten werden durch deine echten Pflanzen ersetzt';
}

17
lib/main.dart Normal file
View file

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'app.dart';
import 'core/settings/settings_provider.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
child: const PlantyApp(),
),
);
}

671
pubspec.lock Normal file
View file

@ -0,0 +1,671 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
url: "https://pub.dev"
source: hosted
version: "91.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
url: "https://pub.dev"
source: hosted
version: "8.4.1"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
url: "https://pub.dev"
source: hosted
version: "1.15.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e"
url: "https://pub.dev"
source: hosted
version: "3.3.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
url: "https://pub.dev"
source: hosted
version: "17.3.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76"
url: "https://pub.dev"
source: hosted
version: "3.3.2"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: transitive
description:
name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
url: "https://pub.dev"
source: hosted
version: "1.26.3"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.7"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
url: "https://pub.dev"
source: hosted
version: "0.6.12"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.38.0"

96
pubspec.yaml Normal file
View file

@ -0,0 +1,96 @@
name: planty
description: "Planty App zur Verwaltung und Pflege von Pflanzen."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.10.7
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_riverpod: ^3.3.2
go_router: ^17.3.0
shared_preferences: ^2.5.5
intl: any
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
generate: true
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

40
test/widget_test.dart Normal file
View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:planty/app.dart';
import 'package:planty/core/settings/settings_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<Widget> buildTestApp() async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
return ProviderScope(
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
child: const PlantyApp(),
);
}
void main() {
testWidgets('App startet mit der Heute-Checkliste', (tester) async {
await tester.pumpWidget(await buildTestApp());
await tester.pumpAndSettle();
// Demo-Daten enthalten überfällige Aufgaben Checkliste sichtbar.
expect(find.text('Heute'), findsOneWidget);
expect(find.textContaining('offene Aufgabe'), findsOneWidget);
});
testWidgets('Aufgabe bestätigen entfernt sie aus der Liste', (tester) async {
await tester.pumpWidget(await buildTestApp());
await tester.pumpAndSettle();
final doneButtons = find.text('Erledigt');
final countBefore = tester.widgetList(doneButtons).length;
expect(countBefore, greaterThan(0));
await tester.tap(doneButtons.first);
await tester.pumpAndSettle();
expect(tester.widgetList(find.text('Erledigt')).length, countBefore - 1);
});
}