V3: Krankheits-Diagnose per Foto (Function + Detail-Screen)
Neue Cloud Function diagnosePlant: Foto + optionale Art an Claude, deutsches JSON-Ergebnis (Befund/Ursache/Behandlung/Vorbeugung). App: PlantDiagnosisService + Untersuchen-Sektion im Pflanzen-Detail mit Ergebnis-Bottom-Sheet und KI-Disclaimer. askClaude max_tokens 1024→2048 (adaptives Denken zählt mit ins Limit). Deploy steht aus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6560232655
commit
18b060413b
7 changed files with 407 additions and 2 deletions
|
|
@ -13,7 +13,7 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si
|
|||
- **Doku (im Repo, deutsch, aktuell halten!):** `README.md`, `docs/architektur.md`, `docs/firebase-einrichtung.md` (Schritt-für-Schritt-Historie aller Backend-Einrichtungen), `docs/git-forgejo.md`
|
||||
- **Claude-Memory:** Projektplan, Nutzerprofil und Backlog liegen im persistenten Memory-Verzeichnis dieser Claude-Code-Installation (werden automatisch geladen)
|
||||
- **Firebase-Projekt:** `leaf-it-to-me-app` (Blaze aktiv, Budget-Alarm eingerichtet; Firebase CLI ist eingeloggt)
|
||||
- **Functions:** `functions/` (TypeScript, Node 22, Region europe-west3): `identifyPlant` (PlantNet + Claude, Secrets `PLANTNET_API_KEY`/`ANTHROPIC_API_KEY` im Secret Manager), `sendDailyReminders` (alle 15 Min, sammelt über alle Haushalte eines Nutzers), `joinHousehold` (Einladungs-Einlösung), `leaveHousehold` (Haushalt verlassen), `removeMember` (Mitglied entfernen, nur Besitzer)
|
||||
- **Functions:** `functions/` (TypeScript, Node 22, Region europe-west3): `identifyPlant` (PlantNet + Claude, Secrets `PLANTNET_API_KEY`/`ANTHROPIC_API_KEY` im Secret Manager), `diagnosePlant` (V3: Krankheits-Diagnose per Foto, Claude Vision), `sendDailyReminders` (alle 15 Min, sammelt über alle Haushalte eines Nutzers), `joinHousehold` (Einladungs-Einlösung), `leaveHousehold` (Haushalt verlassen), `removeMember` (Mitglied entfernen, nur Besitzer)
|
||||
- **Auth:** E-Mail/Passwort + **Anmelden mit Apple** (iOS nativ, Android Web-Flow über Firebase-Handler) + **Anmelden mit Google** (beide Plattformen Web-Flow, eingebauter `GoogleAuthProvider`) — Code in `lib/features/auth/data/auth_repository.dart`, Login-UI in `.../presentation/login_screen.dart`. Haushalts-Bootstrap (Profil + eigener Haushalt beim ersten Login) läuft **reaktiv** über `householdBootstrapProvider` + `BootstrapGate` (Maßstab: `householdId` im users-Doc).
|
||||
|
||||
## Verifizierter Stand
|
||||
|
|
@ -27,6 +27,20 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si
|
|||
|
||||
## Offen / Nächstes
|
||||
|
||||
### 🟡 V3 Baustein 1 — Krankheits-Diagnose per Foto: implementiert, wartet auf Deploy + Gerätetest
|
||||
|
||||
**2026-07-23 implementiert:**
|
||||
- **Function `diagnosePlant`** (`functions/src/index.ts`): Foto (+ optionale Art als `speciesHint`) → Claude beurteilt Gesundheitszustand → JSON `{healthy, summary, details, treatment, prevention}` auf Deutsch. Nutzt den bestehenden `askClaude`-Helper (Modell `claude-sonnet-5`); dessen `max_tokens` von 1024 auf 2048 erhöht (adaptives Denken zählt mit ins Limit).
|
||||
- **App:** `PlantDiagnosisService` (`lib/features/plants/data/plant_diagnosis_service.dart`, lazy Functions-Getter wie beim Household-Repo) + „Pflanze untersuchen"-Sektion im Pflanzen-Detail (`_DiagnosisSection`): Kamera/Galerie-Sheet → Spinner im Button → Ergebnis als scrollbares Bottom-Sheet (Befund mit Ampel-Icon, „Was du tun kannst", „So beugst du vor", KI-Disclaimer). Auch Sitter dürfen den Check nutzen (rein lesend). Fehler der Function (z. B. „keine Pflanze erkannt") werden als Snackbar durchgereicht.
|
||||
- Analyzer + alle 10 Tests grün, `tsc` kompiliert.
|
||||
|
||||
**Noch zu tun (Nutzer):**
|
||||
1. **Deploy** (vom Berechtigungsfilter der Session blockiert, daher manuell): `firebase deploy --only functions:diagnosePlant --project leaf-it-to-me-app`
|
||||
2. Falls der erste Aufruf HTTP 401 liefert: bekannte Gen-2-Stolperfalle (Cloud-Run-Invoker) → dieselbe Function einfach erneut deployen.
|
||||
3. Gerätetest: Pflanze öffnen → „Pflanze untersuchen" → Foto → Ergebnis-Sheet prüfen (gesunde UND kranke Pflanze testen; auch ein Nicht-Pflanzen-Foto → Meldung „keine Pflanze erkannt").
|
||||
|
||||
**Danach V3 weiter:** Stellplatz-Bewertung (Zwei-Ebenen-Konzept), Umtopf-Erinnerungen.
|
||||
|
||||
### 🟡 Google-Login: implementiert, wartet auf Konsolen-Schalter + Gerätetest
|
||||
|
||||
**2026-07-23 implementiert** (`AuthRepository.signInWithGoogle` via eingebautem `GoogleAuthProvider`, Google-Button im Login mit gemeinsamem Provider-Handler `_signInWithProvider`, l10n-Strings, iOS-URL-Scheme/Encoded App ID in `Info.plist`, Doku `firebase-einrichtung.md` Schritt 8). Analyzer + alle 10 Tests grün. **Nutzer muss noch:** In der Firebase-Konsole *Authentication → Sign-in method → Google* aktivieren (Support-E-Mail wählen — der einzige Konfigurationsschritt), dann auf iPhone und Android testen; der erste Google-Login muss kurz den Einrichtungs-Bildschirm zeigen (reaktiver Bootstrap deckt Google automatisch ab).
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ async function askClaude<T>(
|
|||
},
|
||||
body: JSON.stringify({
|
||||
model: ANTHROPIC_MODEL,
|
||||
max_tokens: 1024,
|
||||
// Claude Sonnet 5 denkt standardmäßig adaptiv mit; das Limit deckt
|
||||
// Denk- UND Antwort-Tokens ab — großzügig, damit nichts abreißt.
|
||||
max_tokens: 2048,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
|
|
@ -127,6 +129,79 @@ async function askClaude<T>(
|
|||
* 2. Bei zu geringer Konfidenz bestimmt Claude die Art als Fallback.
|
||||
* 3. Claude erstellt Beschreibung, Pflegehinweise und Intervall-Vorschläge.
|
||||
*/
|
||||
interface DiagnosisResult {
|
||||
healthy: boolean;
|
||||
summary: string;
|
||||
details: string;
|
||||
treatment: string;
|
||||
prevention: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Krankheits-Diagnose per Foto (V3).
|
||||
*
|
||||
* Claude beurteilt den Gesundheitszustand der Pflanze auf dem Foto und
|
||||
* liefert Befund, Ursache, Behandlung und Vorbeugung auf Deutsch.
|
||||
* Optional kann die App die bekannte Art mitgeben (speciesHint).
|
||||
*/
|
||||
export const diagnosePlant = onCall(
|
||||
{
|
||||
region: "europe-west3",
|
||||
secrets: [anthropicApiKey],
|
||||
memory: "512MiB",
|
||||
timeoutSeconds: 120,
|
||||
maxInstances: 5,
|
||||
},
|
||||
async (request) => {
|
||||
if (!request.auth) {
|
||||
throw new HttpsError("unauthenticated", "Anmeldung erforderlich.");
|
||||
}
|
||||
const imageBase64 = request.data?.imageBase64;
|
||||
if (typeof imageBase64 !== "string" || imageBase64.length === 0) {
|
||||
throw new HttpsError("invalid-argument", "imageBase64 fehlt.");
|
||||
}
|
||||
if (imageBase64.length > 8_000_000) {
|
||||
throw new HttpsError("invalid-argument", "Bild ist zu groß.");
|
||||
}
|
||||
const speciesHint =
|
||||
typeof request.data?.speciesHint === "string" ?
|
||||
request.data.speciesHint.slice(0, 200) :
|
||||
"";
|
||||
|
||||
const speciesPart = speciesHint ?
|
||||
`Es handelt sich vermutlich um "${speciesHint}". ` :
|
||||
"";
|
||||
const diagnosis = await askClaude<DiagnosisResult>(
|
||||
imageBase64,
|
||||
"Du bist ein erfahrener Pflanzenarzt. Beurteile den Gesundheitszustand " +
|
||||
`der Pflanze auf dem Foto. ${speciesPart}` +
|
||||
"Achte auf Blattverfärbungen, Flecken, Schädlinge, Schimmel, " +
|
||||
"Trockenheit, Überwässerung und ähnliche Anzeichen. " +
|
||||
"Antworte auf Deutsch, in einfacher, freundlicher Sprache " +
|
||||
"(auch für ältere Menschen gut verständlich, keine Fachbegriffe ohne " +
|
||||
"Erklärung). Antworte NUR mit JSON im Format " +
|
||||
'{"healthy": <true wenn die Pflanze gesund wirkt, sonst false>, ' +
|
||||
'"summary": "<Kurzbefund in 1 Satz>", ' +
|
||||
'"details": "<2-3 Sätze: was ist zu sehen und was ist die ' +
|
||||
'wahrscheinliche Ursache>", ' +
|
||||
'"treatment": "<2-4 Sätze mit konkreten Behandlungs-Schritten; bei ' +
|
||||
'gesunder Pflanze kurze Pflegetipps>", ' +
|
||||
'"prevention": "<1-2 Sätze, wie man dem Problem vorbeugt>"}. ' +
|
||||
"Wenn auf dem Foto keine Pflanze erkennbar ist, setze summary auf " +
|
||||
"einen leeren String.",
|
||||
anthropicApiKey.value()
|
||||
);
|
||||
|
||||
if (!diagnosis.summary) {
|
||||
throw new HttpsError(
|
||||
"not-found",
|
||||
"Auf dem Foto wurde keine Pflanze erkannt."
|
||||
);
|
||||
}
|
||||
return diagnosis;
|
||||
}
|
||||
);
|
||||
|
||||
export const identifyPlant = onCall(
|
||||
{
|
||||
region: "europe-west3",
|
||||
|
|
|
|||
58
lib/features/plants/data/plant_diagnosis_service.dart
Normal file
58
lib/features/plants/data/plant_diagnosis_service.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cloud_functions/cloud_functions.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// Ergebnis der KI-Krankheits-Diagnose (Cloud Function diagnosePlant).
|
||||
class DiagnosisResult {
|
||||
const DiagnosisResult({
|
||||
required this.healthy,
|
||||
required this.summary,
|
||||
required this.details,
|
||||
required this.treatment,
|
||||
required this.prevention,
|
||||
});
|
||||
|
||||
final bool healthy;
|
||||
final String summary;
|
||||
final String details;
|
||||
final String treatment;
|
||||
final String prevention;
|
||||
}
|
||||
|
||||
class PlantDiagnosisService {
|
||||
PlantDiagnosisService(this._functionsGetter);
|
||||
|
||||
// Lazy, damit Widget-Tests ohne initialisiertes Firebase auskommen —
|
||||
// FirebaseFunctions.instanceFor wirft sonst schon beim Erzeugen.
|
||||
final FirebaseFunctions Function() _functionsGetter;
|
||||
|
||||
/// Schickt das Foto an die Cloud Function (Claude) – dauert einige Sekunden.
|
||||
/// [speciesHint] ist die bekannte Art der Pflanze und hilft der Diagnose.
|
||||
Future<DiagnosisResult> diagnose(
|
||||
Uint8List imageBytes, {
|
||||
String? speciesHint,
|
||||
}) async {
|
||||
final callable = _functionsGetter().httpsCallable('diagnosePlant');
|
||||
final response = await callable.call<Map<String, dynamic>>({
|
||||
'imageBase64': base64Encode(imageBytes),
|
||||
if (speciesHint != null && speciesHint.trim().isNotEmpty)
|
||||
'speciesHint': speciesHint.trim(),
|
||||
});
|
||||
final data = response.data;
|
||||
return DiagnosisResult(
|
||||
healthy: data['healthy'] as bool? ?? false,
|
||||
summary: data['summary'] as String? ?? '',
|
||||
details: data['details'] as String? ?? '',
|
||||
treatment: data['treatment'] as String? ?? '',
|
||||
prevention: data['prevention'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final plantDiagnosisServiceProvider = Provider<PlantDiagnosisService>((ref) {
|
||||
return PlantDiagnosisService(
|
||||
() => FirebaseFunctions.instanceFor(region: 'europe-west3'),
|
||||
);
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:cloud_functions/cloud_functions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../core/router/app_router.dart';
|
||||
|
|
@ -8,7 +10,9 @@ import '../../../l10n/generated/app_localizations.dart';
|
|||
import '../../household/data/household_providers.dart';
|
||||
import '../../household/domain/household.dart';
|
||||
import '../../locations/data/locations_provider.dart';
|
||||
import '../data/plant_diagnosis_service.dart';
|
||||
import '../data/plants_provider.dart';
|
||||
import '../domain/plant.dart';
|
||||
|
||||
class PlantDetailScreen extends ConsumerWidget {
|
||||
const PlantDetailScreen({super.key, required this.plantId});
|
||||
|
|
@ -144,12 +148,193 @@ class PlantDetailScreen extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
],
|
||||
const Divider(height: 32),
|
||||
_DiagnosisSection(plant: plant),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Krankheits-Check (V3): Foto aufnehmen → Claude beurteilt den Zustand
|
||||
/// und gibt Behandlungstipps. Auch für Sitter nutzbar (nur lesend).
|
||||
class _DiagnosisSection extends ConsumerStatefulWidget {
|
||||
const _DiagnosisSection({required this.plant});
|
||||
|
||||
final Plant plant;
|
||||
|
||||
@override
|
||||
ConsumerState<_DiagnosisSection> createState() => _DiagnosisSectionState();
|
||||
}
|
||||
|
||||
class _DiagnosisSectionState extends ConsumerState<_DiagnosisSection> {
|
||||
final _picker = ImagePicker();
|
||||
bool _busy = false;
|
||||
|
||||
Future<void> _start() async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final source = await showModalBottomSheet<ImageSource>(
|
||||
context: context,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_camera),
|
||||
title: Text(l10n.takePhoto),
|
||||
onTap: () => Navigator.pop(sheetContext, ImageSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: Text(l10n.fromGallery),
|
||||
onTap: () => Navigator.pop(sheetContext, ImageSource.gallery),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (source == null || !mounted) return;
|
||||
|
||||
final picked = await _picker.pickImage(
|
||||
source: source,
|
||||
maxWidth: 1280,
|
||||
imageQuality: 80,
|
||||
);
|
||||
if (picked == null || !mounted) return;
|
||||
final bytes = await picked.readAsBytes();
|
||||
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
final result = await ref.read(plantDiagnosisServiceProvider).diagnose(
|
||||
bytes,
|
||||
speciesHint: widget.plant.species,
|
||||
);
|
||||
if (!mounted) return;
|
||||
await _showResult(result);
|
||||
} on FirebaseFunctionsException catch (e) {
|
||||
if (!mounted) return;
|
||||
// Die Function liefert deutsche Meldungen (z. B. „keine Pflanze
|
||||
// erkannt") – die zeigen wir direkt an.
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(e.message ?? l10n.diagnosisFailed),
|
||||
));
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(content: Text(l10n.diagnosisFailed)));
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showResult(DiagnosisResult result) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (sheetContext) {
|
||||
final theme = Theme.of(sheetContext);
|
||||
final healthyColor = theme.brightness == Brightness.dark
|
||||
? Colors.green.shade300
|
||||
: Colors.green.shade700;
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.6,
|
||||
maxChildSize: 0.95,
|
||||
builder: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
result.healthy
|
||||
? Icons.check_circle
|
||||
: Icons.warning_amber_rounded,
|
||||
size: 32,
|
||||
color: result.healthy
|
||||
? healthyColor
|
||||
: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
result.summary,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (result.details.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(result.details, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
if (result.treatment.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.diagnosisTreatmentTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(result.treatment, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
if (result.prevention.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.diagnosisPreventionTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(result.prevention, style: theme.textTheme.bodyLarge),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.diagnosisDisclaimer,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: _busy ? null : _start,
|
||||
icon: _busy
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.health_and_safety_outlined),
|
||||
label: Text(_busy ? l10n.diagnosing : l10n.diagnoseButton),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.diagnoseHint,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoTile extends StatelessWidget {
|
||||
const _InfoTile({
|
||||
required this.icon,
|
||||
|
|
|
|||
|
|
@ -227,6 +227,13 @@
|
|||
}
|
||||
},
|
||||
"recognitionFailed": "Die Erkennung hat nicht geklappt – du kannst die Angaben auch von Hand eintragen.",
|
||||
"diagnoseButton": "Pflanze untersuchen",
|
||||
"diagnoseHint": "Foto von kranken Blättern oder Stellen aufnehmen – die KI beurteilt den Zustand und gibt Behandlungstipps.",
|
||||
"diagnosing": "Foto wird untersucht …",
|
||||
"diagnosisTreatmentTitle": "Was du tun kannst",
|
||||
"diagnosisPreventionTitle": "So beugst du vor",
|
||||
"diagnosisDisclaimer": "Die Einschätzung kommt von einer KI und kann falsch liegen. Bei wertvollen Pflanzen im Zweifel eine Gärtnerei fragen.",
|
||||
"diagnosisFailed": "Die Untersuchung hat nicht geklappt. Bitte versuche es erneut.",
|
||||
"saveError": "Speichern fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"saveErrorDetail": "Speichern fehlgeschlagen ({detail}). Bitte versuche es erneut.",
|
||||
"@saveErrorDetail": {
|
||||
|
|
|
|||
|
|
@ -826,6 +826,48 @@ abstract class AppLocalizations {
|
|||
/// **'Die Erkennung hat nicht geklappt – du kannst die Angaben auch von Hand eintragen.'**
|
||||
String get recognitionFailed;
|
||||
|
||||
/// No description provided for @diagnoseButton.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Pflanze untersuchen'**
|
||||
String get diagnoseButton;
|
||||
|
||||
/// No description provided for @diagnoseHint.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Foto von kranken Blättern oder Stellen aufnehmen – die KI beurteilt den Zustand und gibt Behandlungstipps.'**
|
||||
String get diagnoseHint;
|
||||
|
||||
/// No description provided for @diagnosing.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Foto wird untersucht …'**
|
||||
String get diagnosing;
|
||||
|
||||
/// No description provided for @diagnosisTreatmentTitle.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Was du tun kannst'**
|
||||
String get diagnosisTreatmentTitle;
|
||||
|
||||
/// No description provided for @diagnosisPreventionTitle.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'So beugst du vor'**
|
||||
String get diagnosisPreventionTitle;
|
||||
|
||||
/// No description provided for @diagnosisDisclaimer.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Die Einschätzung kommt von einer KI und kann falsch liegen. Bei wertvollen Pflanzen im Zweifel eine Gärtnerei fragen.'**
|
||||
String get diagnosisDisclaimer;
|
||||
|
||||
/// No description provided for @diagnosisFailed.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Die Untersuchung hat nicht geklappt. Bitte versuche es erneut.'**
|
||||
String get diagnosisFailed;
|
||||
|
||||
/// No description provided for @saveError.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
|
|
|
|||
|
|
@ -449,6 +449,30 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get recognitionFailed =>
|
||||
'Die Erkennung hat nicht geklappt – du kannst die Angaben auch von Hand eintragen.';
|
||||
|
||||
@override
|
||||
String get diagnoseButton => 'Pflanze untersuchen';
|
||||
|
||||
@override
|
||||
String get diagnoseHint =>
|
||||
'Foto von kranken Blättern oder Stellen aufnehmen – die KI beurteilt den Zustand und gibt Behandlungstipps.';
|
||||
|
||||
@override
|
||||
String get diagnosing => 'Foto wird untersucht …';
|
||||
|
||||
@override
|
||||
String get diagnosisTreatmentTitle => 'Was du tun kannst';
|
||||
|
||||
@override
|
||||
String get diagnosisPreventionTitle => 'So beugst du vor';
|
||||
|
||||
@override
|
||||
String get diagnosisDisclaimer =>
|
||||
'Die Einschätzung kommt von einer KI und kann falsch liegen. Bei wertvollen Pflanzen im Zweifel eine Gärtnerei fragen.';
|
||||
|
||||
@override
|
||||
String get diagnosisFailed =>
|
||||
'Die Untersuchung hat nicht geklappt. Bitte versuche es erneut.';
|
||||
|
||||
@override
|
||||
String get saveError => 'Speichern fehlgeschlagen. Bitte versuche es erneut.';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue