diff --git a/SPEC.md b/SPEC.md index ba4f972..08430b3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -37,6 +37,10 @@ - http (REST client for Cookidoo recipe search and detail APIs) +## Connectivity + +- connectivity_plus (network type detection before model download) + ## Chat UI - flutter_chat_ui (Flyer Chat v2 — message list, composer, streaming) @@ -58,7 +62,7 @@ ## Internationalization -- flutter_localizations (ARB-based, 4 locales: en, fr, de, es) +- flutter_localizations (ARB-based, 5 locales: en, fr, de, es, it) ## Build & CI diff --git a/docs/superpowers/plans/2026-04-26-connectivity-warning-before-model-download.md b/docs/superpowers/plans/2026-04-26-connectivity-warning-before-model-download.md new file mode 100644 index 0000000..b818fa7 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-connectivity-warning-before-model-download.md @@ -0,0 +1,264 @@ +# Connectivity Warning Before Model Download — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Warn users before starting the LLM model download when they are on mobile data or have no internet connection. + +**Architecture:** A one-shot connectivity check using `connectivity_plus` in `ModelDownloadPage._startDownload()`, gating the download behind an informative dialog. No new services, providers, or streams. + +**Tech Stack:** connectivity_plus, Flutter Material dialogs, ARB localization + +--- + +### Task 1: Add connectivity_plus dependency + +**Files:** +- Modify: `pubspec.yaml:56` (add dependency) + +- [ ] **Step 1: Add the package** + +In `pubspec.yaml`, add `connectivity_plus` after `firebase_performance`: + +```yaml + firebase_performance: ^0.10.0+12 + connectivity_plus: ^6.1.4 +``` + +- [ ] **Step 2: Install** + +Run: `flutter pub get` +Expected: resolves successfully, no version conflicts. + +- [ ] **Step 3: Commit** + +``` +feat(chat): add connectivity_plus dependency +``` + +--- + +### Task 2: Add localization strings + +**Files:** +- Modify: `lib/l10n/app_en.arb:464-465` +- Modify: `lib/l10n/app_es.arb:154-155` +- Modify: `lib/l10n/app_fr.arb:154-155` +- Modify: `lib/l10n/app_de.arb:154-155` + +Five new keys are needed. Existing keys `cancel`, `ok` are reused. + +- [ ] **Step 1: Add English strings** + +In `lib/l10n/app_en.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "chatModelDownloadNoConnectionTitle": "No internet connection", + "@chatModelDownloadNoConnectionTitle": { "description": "Dialog title when there is no internet connection before model download." }, + + "chatModelDownloadNoConnectionBody": "Connect to the internet to download the AI model.", + "@chatModelDownloadNoConnectionBody": { "description": "Dialog body when there is no internet connection before model download." }, + + "chatModelDownloadMobileDataTitle": "Large download", + "@chatModelDownloadMobileDataTitle": { "description": "Dialog title warning about large download on mobile data." }, + + "chatModelDownloadMobileDataBody": "The AI model download is large. A WiFi connection is recommended.", + "@chatModelDownloadMobileDataBody": { "description": "Dialog body warning about large download on mobile data." }, + + "chatModelDownloadContinue": "Continue", + "@chatModelDownloadContinue": { "description": "Button label to proceed with download on mobile data." } +``` + +- [ ] **Step 2: Add Spanish strings** + +In `lib/l10n/app_es.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "chatModelDownloadNoConnectionTitle": "Sin conexión a internet", + "chatModelDownloadNoConnectionBody": "Conéctate a internet para descargar el modelo de IA.", + "chatModelDownloadMobileDataTitle": "Descarga grande", + "chatModelDownloadMobileDataBody": "La descarga del modelo de IA es grande. Se recomienda usar una conexión WiFi.", + "chatModelDownloadContinue": "Continuar" +``` + +- [ ] **Step 3: Add French strings** + +In `lib/l10n/app_fr.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "chatModelDownloadNoConnectionTitle": "Pas de connexion internet", + "chatModelDownloadNoConnectionBody": "Connectez-vous à internet pour télécharger le modèle IA.", + "chatModelDownloadMobileDataTitle": "Téléchargement volumineux", + "chatModelDownloadMobileDataBody": "Le téléchargement du modèle IA est volumineux. Une connexion WiFi est recommandée.", + "chatModelDownloadContinue": "Continuer" +``` + +- [ ] **Step 4: Add German strings** + +In `lib/l10n/app_de.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "chatModelDownloadNoConnectionTitle": "Keine Internetverbindung", + "chatModelDownloadNoConnectionBody": "Verbinde dich mit dem Internet, um das KI-Modell herunterzuladen.", + "chatModelDownloadMobileDataTitle": "Großer Download", + "chatModelDownloadMobileDataBody": "Der Download des KI-Modells ist groß. Eine WLAN-Verbindung wird empfohlen.", + "chatModelDownloadContinue": "Weiter" +``` + +- [ ] **Step 5: Regenerate localizations** + +Run: `flutter gen-l10n` +Expected: completes without errors, generates updated `AppLocalizations`. + +- [ ] **Step 6: Commit** + +``` +feat(l10n): add connectivity warning strings for model download +``` + +--- + +### Task 3: Add connectivity check and dialogs to ModelDownloadPage + +**Files:** +- Modify: `lib/features/chat/presentation/model_download_page.dart` + +- [ ] **Step 1: Add imports** + +At the top of `model_download_page.dart`, add the `connectivity_plus` import: + +```dart +import 'package:connectivity_plus/connectivity_plus.dart'; +``` + +- [ ] **Step 2: Add the connectivity check method** + +Inside `_ModelDownloadPageState`, add a new method after `_startDownload()`: + +```dart + /// Returns true if the download should proceed, false otherwise. + Future _checkConnectivity() async { + final results = await Connectivity().checkConnectivity(); + + if (results.contains(ConnectivityResult.none)) { + if (!mounted) return false; + final l10n = AppLocalizations.of(context); + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.chatModelDownloadNoConnectionTitle), + content: Text(l10n.chatModelDownloadNoConnectionBody), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.ok), + ), + ], + ), + ); + return false; + } + + if (!results.contains(ConnectivityResult.wifi)) { + if (!mounted) return false; + final l10n = AppLocalizations.of(context); + final proceed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.chatModelDownloadMobileDataTitle), + content: Text(l10n.chatModelDownloadMobileDataBody), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.chatModelDownloadContinue), + ), + ], + ), + ); + return proceed ?? false; + } + + return true; + } +``` + +- [ ] **Step 3: Gate the download behind the connectivity check** + +Modify `_startDownload()` to call `_checkConnectivity()` before downloading. Replace the current method body: + +```dart + Future _startDownload() async { + setState(() { + _error = null; + _progress = 0; + }); + + final shouldProceed = await _checkConnectivity(); + if (!shouldProceed) return; + + try { + final model = await ref.read(chatModelPreferenceProvider.future); + + await FlutterGemma.installModel( + modelType: model.modelType, + fileType: model.fileType, + ).fromNetwork(model.url).withProgress((progress) { + if (mounted) { + setState(() => _progress = progress); + } + }).install(); + + final storage = + await ref.read(chatModelPreferenceStorageProvider.future); + await storage.writeInstalled(model); + + if (mounted) { + widget.onComplete(); + } + } catch (e, stack) { + debugPrint('Model download failed: $e\n$stack'); + if (mounted) { + setState(() { + _error = e.toString(); + }); + } + } + } +``` + +- [ ] **Step 4: Verify the build compiles** + +Run: `flutter build apk --debug 2>&1 | tail -5` +Expected: `BUILD SUCCESSFUL` + +- [ ] **Step 5: Commit** + +``` +feat(chat): add connectivity check before model download +``` + +--- + +### Task 4: Update SPEC.md + +**Files:** +- Modify: `SPEC.md` + +- [ ] **Step 1: Add connectivity_plus to the Networking section** + +In `SPEC.md`, add a new section after **Cookidoo Integration** and before **Chat UI**: + +```markdown +## Connectivity + +- connectivity_plus (network type detection before model download) +``` + +- [ ] **Step 2: Commit** + +``` +docs: add connectivity_plus to SPEC.md +``` diff --git a/docs/superpowers/plans/2026-04-26-reset-all-settings.md b/docs/superpowers/plans/2026-04-26-reset-all-settings.md new file mode 100644 index 0000000..8086ee0 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-reset-all-settings.md @@ -0,0 +1,207 @@ +# Reset All Settings — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a "Reset all settings" action that wipes all local data and closes the app. + +**Architecture:** A new list tile in the Settings Actions section triggers a confirmation dialog. On confirm, SharedPreferences are cleared, the downloaded model is uninstalled, the SQLite database is deleted, and the app closes via `SystemNavigator.pop()`. + +**Tech Stack:** SharedPreferences, sqflite (deleteDatabase), flutter_gemma (uninstallModel), Flutter services (SystemNavigator) + +--- + +### Task 1: Add localization strings + +**Files:** +- Modify: `lib/l10n/app_en.arb` +- Modify: `lib/l10n/app_es.arb` +- Modify: `lib/l10n/app_fr.arb` +- Modify: `lib/l10n/app_de.arb` +- Modify: `lib/l10n/app_it.arb` + +Three new keys. Existing key `cancel` is reused. + +- [ ] **Step 1: Add English strings** + +In `lib/l10n/app_en.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "settingsResetAll": "Reset all settings", + "@settingsResetAll": { "description": "Label for the reset all settings action in settings." }, + + "settingsResetAllConfirmation": "Reset all settings? The app will close and all data will be deleted. This cannot be undone.", + "@settingsResetAllConfirmation": { "description": "Confirmation prompt before resetting all settings." }, + + "settingsResetAllButton": "Reset", + "@settingsResetAllButton": { "description": "Button label to confirm reset all settings." } +``` + +- [ ] **Step 2: Add Spanish strings** + +In `lib/l10n/app_es.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "settingsResetAll": "Restablecer todos los ajustes", + "settingsResetAllConfirmation": "¿Restablecer todos los ajustes? La aplicación se cerrará y todos los datos serán eliminados. Esta acción no se puede deshacer.", + "settingsResetAllButton": "Restablecer" +``` + +- [ ] **Step 3: Add French strings** + +In `lib/l10n/app_fr.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "settingsResetAll": "Réinitialiser tous les réglages", + "settingsResetAllConfirmation": "Réinitialiser tous les réglages ? L'application se fermera et toutes les données seront supprimées. Cette action est irréversible.", + "settingsResetAllButton": "Réinitialiser" +``` + +- [ ] **Step 4: Add German strings** + +In `lib/l10n/app_de.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "settingsResetAll": "Alle Einstellungen zurücksetzen", + "settingsResetAllConfirmation": "Alle Einstellungen zurücksetzen? Die App wird geschlossen und alle Daten werden gelöscht. Dies kann nicht rückgängig gemacht werden.", + "settingsResetAllButton": "Zurücksetzen" +``` + +- [ ] **Step 5: Add Italian strings** + +In `lib/l10n/app_it.arb`, before the closing `}`, add a comma after the last entry and insert: + +```json + "settingsResetAll": "Ripristina tutte le impostazioni", + "settingsResetAllConfirmation": "Ripristinare tutte le impostazioni? L'app si chiuderà e tutti i dati verranno eliminati. Questa azione non può essere annullata.", + "settingsResetAllButton": "Ripristina" +``` + +- [ ] **Step 6: Regenerate localizations** + +Run: `flutter gen-l10n` +Expected: completes without errors. + +- [ ] **Step 7: Commit** + +``` +feat(l10n): add reset all settings strings +``` + +--- + +### Task 2: Add reset action to settings page + +**Files:** +- Modify: `lib/features/settings/presentation/settings_page.dart` + +- [ ] **Step 1: Add imports** + +At the top of `settings_page.dart`, add these imports: + +```dart +import 'package:flutter/services.dart'; +import 'package:flutter_gemma/flutter_gemma.dart'; +import 'package:path/path.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite/sqflite.dart'; +``` + +- [ ] **Step 2: Add the reset tile in the build method** + +In the `build` method, after the existing "Delete all conversations" `ListTile` (line 89-101) and before `const Divider(height: 1)` at line 102, insert a new tile: + +```dart + const Divider(height: 1), + ListTile( + leading: Icon( + Icons.restore, + color: Theme.of(context).colorScheme.error, + ), + title: Text( + l10n.settingsResetAll, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + onTap: () => _confirmResetAll(context, ref), + ), +``` + +- [ ] **Step 3: Add the _confirmResetAll method** + +Add a new method after `_confirmDeleteAll` (after line 134), before the closing `}` of the class: + +```dart + Future _confirmResetAll(BuildContext context, WidgetRef ref) async { + final l10n = AppLocalizations.of(context); + final confirmed = await showDialog( + context: context, + useRootNavigator: true, + builder: (ctx) => AlertDialog( + title: Text(l10n.settingsResetAll), + content: Text(l10n.settingsResetAllConfirmation), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: Text( + l10n.settingsResetAllButton, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ), + ], + ), + ); + if (confirmed != true) return; + + // 1. Read installed model before clearing preferences. + final prefs = await SharedPreferences.getInstance(); + final installedModelName = prefs.getString('chat_model_installed'); + + // 2. Clear all shared preferences. + await prefs.clear(); + + // 3. Uninstall downloaded model if one exists. + if (installedModelName != null) { + for (final model in ChatModelPreference.values) { + if (model.name == installedModelName) { + try { + await FlutterGemma.uninstallModel(model.fileName); + } catch (_) { + // Best-effort — model file may already be gone. + } + break; + } + } + } + + // 4. Delete SQLite database. + final dbPath = join(await getDatabasesPath(), 'cookmate_chat.db'); + await deleteDatabase(dbPath); + + // 5. Close the app. + await SystemNavigator.pop(); + } +``` + +- [ ] **Step 4: Add the missing import for ChatModelPreference** + +Add this import at the top of the file: + +```dart +import '../../chat/domain/chat_model_preference.dart'; +``` + +- [ ] **Step 5: Verify the build compiles** + +Run: `flutter build apk --debug 2>&1 | tail -5` +Expected: `BUILD SUCCESSFUL` + +- [ ] **Step 6: Commit** + +``` +feat(settings): add reset all settings action +``` diff --git a/docs/superpowers/specs/2026-04-26-connectivity-warning-before-model-download-design.md b/docs/superpowers/specs/2026-04-26-connectivity-warning-before-model-download-design.md new file mode 100644 index 0000000..325e3b4 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-connectivity-warning-before-model-download-design.md @@ -0,0 +1,88 @@ +# Connectivity Warning Before Model Download + +## Problem + +The LLM model download (1.5–3 GB) starts automatically without checking network +conditions. Users on mobile data may unknowingly consume a large amount of their +data plan. Users with no connection see a generic error only after the download +fails. + +## Solution + +Before starting the model download, check the device's connectivity state and +show an informative dialog when conditions are not ideal. + +## Behavior + +### WiFi connected + +Download starts normally — no dialog shown. + +### Mobile data (no WiFi) + +An informative dialog is displayed: + +- **Title:** Large download warning (localized) +- **Body:** The model download is large. A WiFi connection is recommended. + (localized) +- **Actions:** "Cancel" (closes dialog, download does not start) / "Continue" + (closes dialog, download starts) + +The download does **not** start until the user taps "Continue". + +### No connection + +An informative dialog is displayed: + +- **Title:** No connection (localized) +- **Body:** No internet connection. Connect to the internet to download the + model. (localized) +- **Actions:** "OK" (closes dialog, download does not start) + +The download does not start. The user can tap "Retry" (existing button from +error state) to re-trigger the connectivity check. + +## Architecture + +### Package + +- `connectivity_plus` added to `pubspec.yaml` + +### Implementation + +All logic lives in `ModelDownloadPage._startDownload()`: + +1. Call `Connectivity().checkConnectivity()` to get the current connectivity + result. +2. If the result contains only `ConnectivityResult.none` → show "no connection" + dialog, return early. +3. If the result does not contain `ConnectivityResult.wifi` (i.e., mobile data + only) → show "large download" dialog. If user cancels, return early. +4. Otherwise (WiFi present) → proceed to download. + +No new service or provider is needed — this is a single check at a single call +site. + +### Localization + +New ARB keys added to all four locale files (`en`, `es`, `fr`, `de`): + +| Key | EN value | +|-----|----------| +| `chatModelDownloadNoConnectionTitle` | No internet connection | +| `chatModelDownloadNoConnectionBody` | Connect to the internet to download the AI model. | +| `chatModelDownloadMobileDataTitle` | Large download | +| `chatModelDownloadMobileDataBody` | The AI model download is large. A WiFi connection is recommended. | +| `chatModelDownloadContinue` | Continue | + +Existing keys reused: `cancel`, `ok`. + +### SPEC.md + +Add `connectivity_plus` to the listed dependencies. + +## What is NOT in scope + +- No continuous connectivity monitoring (stream) +- No internet quality / reachability verification +- No display of downloaded or total file size diff --git a/docs/superpowers/specs/2026-04-26-reset-all-settings-design.md b/docs/superpowers/specs/2026-04-26-reset-all-settings-design.md new file mode 100644 index 0000000..bd3e24f --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-reset-all-settings-design.md @@ -0,0 +1,58 @@ +# Reset All Settings + +## Problem + +Users have no way to fully reset the app to its initial state. The only +destructive action available is "Delete all conversations", which leaves +preferences, downloaded models, and credentials intact. + +## Solution + +Add a "Reset all settings" action in the Settings Actions section that wipes +all local data and closes the app. + +## UI + +New list tile in Settings > Actions, below "Delete all conversations": + +- Icon: `Icons.restore`, red color +- Label: "Reset all settings" (localized) +- Tap shows a confirmation dialog: + - Body: "Reset all settings? The app will close and all data will be deleted. + This cannot be undone." (localized) + - Actions: "Cancel" (existing key) / "Reset" (localized, red/destructive) + +## Logic + +Executed sequentially after user confirms: + +1. **Clear SharedPreferences** — `SharedPreferences.getInstance()` then + `prefs.clear()`. Removes all key-value settings (theme, locale, model + preference, backend, reasoning, expert config, skills, Cookidoo credentials, + observability toggles). +2. **Uninstall downloaded model** — Read installed model from + `chatModelPreferenceStorageProvider`. If a model is installed, call + `FlutterGemma.uninstallModel(fileName)`. +3. **Delete SQLite database** — Call `deleteDatabase(path)` using the same + database path from `ChatDatabase` (`cookmate_chat.db`). +4. **Close the app** — `SystemNavigator.pop()`. + +All logic lives directly in `settings_page.dart` — no new service needed. + +## Localization + +New keys in all 5 locale files (en, es, fr, de, it): + +| Key | EN value | +|-----|----------| +| `settingsResetAll` | Reset all settings | +| `settingsResetAllConfirmation` | Reset all settings? The app will close and all data will be deleted. This cannot be undone. | +| `settingsResetAllButton` | Reset | + +Existing key reused: `cancel`. + +## What is NOT in scope + +- No selective reset (everything is wiped) +- No redirect to splash screen (app closes) +- No dedicated service or provider diff --git a/lib/features/chat/presentation/conversation_page.dart b/lib/features/chat/presentation/conversation_page.dart index a5a47eb..2e7dffc 100644 --- a/lib/features/chat/presentation/conversation_page.dart +++ b/lib/features/chat/presentation/conversation_page.dart @@ -58,6 +58,7 @@ class _ConversationPageState extends ConsumerState { 'en': 'English', 'es': 'Español', 'de': 'Deutsch', + 'it': 'Italiano', }; void _clearPendingAudio() { diff --git a/lib/features/chat/presentation/model_download_page.dart b/lib/features/chat/presentation/model_download_page.dart index bf17c32..d68f2ea 100644 --- a/lib/features/chat/presentation/model_download_page.dart +++ b/lib/features/chat/presentation/model_download_page.dart @@ -1,3 +1,4 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:cookmate/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gemma/flutter_gemma.dart'; @@ -17,20 +18,84 @@ class ModelDownloadPage extends ConsumerStatefulWidget { class _ModelDownloadPageState extends ConsumerState { int _progress = 0; String? _error; + bool _downloading = false; @override void initState() { super.initState(); - _startDownload(); + Future.microtask(_startDownload); + } + + /// Returns true if the download should proceed, false otherwise. + Future _checkConnectivity() async { + final results = await Connectivity().checkConnectivity(); + + if (results.contains(ConnectivityResult.none)) { + if (!mounted) return false; + final l10n = AppLocalizations.of(context); + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: Text(l10n.chatModelDownloadNoConnectionTitle), + content: Text(l10n.chatModelDownloadNoConnectionBody), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.ok), + ), + ], + ), + ); + return false; + } + + if (results.contains(ConnectivityResult.mobile) && + !results.contains(ConnectivityResult.wifi)) { + if (!mounted) return false; + final l10n = AppLocalizations.of(context); + final proceed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.chatModelDownloadMobileDataTitle), + content: Text(l10n.chatModelDownloadMobileDataBody), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.chatModelDownloadContinue), + ), + ], + ), + ); + return proceed ?? false; + } + + return true; } Future _startDownload() async { + if (_downloading) return; + _downloading = true; setState(() { _error = null; _progress = 0; }); try { + final shouldProceed = await _checkConnectivity(); + if (!shouldProceed) { + if (mounted) { + setState(() { + _error = 'connectivity'; + }); + } + return; + } + final model = await ref.read(chatModelPreferenceProvider.future); await FlutterGemma.installModel( @@ -56,6 +121,8 @@ class _ModelDownloadPageState extends ConsumerState { _error = e.toString(); }); } + } finally { + _downloading = false; } } diff --git a/lib/features/l10n/domain/locale_preference.dart b/lib/features/l10n/domain/locale_preference.dart index 3a50337..d8f6af3 100644 --- a/lib/features/l10n/domain/locale_preference.dart +++ b/lib/features/l10n/domain/locale_preference.dart @@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart'; sealed class LocalePreference { const LocalePreference(); - static const Set supportedLanguageCodes = {'en', 'fr', 'es', 'de'}; + static const Set supportedLanguageCodes = {'en', 'fr', 'es', 'de', 'it'}; String toStorageValue(); diff --git a/lib/features/l10n/presentation/language_picker_tile.dart b/lib/features/l10n/presentation/language_picker_tile.dart index 9b40857..001b36c 100644 --- a/lib/features/l10n/presentation/language_picker_tile.dart +++ b/lib/features/l10n/presentation/language_picker_tile.dart @@ -10,6 +10,7 @@ const Map _languageNames = { 'en': 'English', 'es': 'Español', 'de': 'Deutsch', + 'it': 'Italiano', }; class LanguagePickerTile extends ConsumerWidget { diff --git a/lib/features/settings/presentation/settings_page.dart b/lib/features/settings/presentation/settings_page.dart index a77a667..437c626 100644 --- a/lib/features/settings/presentation/settings_page.dart +++ b/lib/features/settings/presentation/settings_page.dart @@ -1,6 +1,11 @@ import 'package:cookmate/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gemma/flutter_gemma.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path/path.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite/sqflite.dart'; import '../../chat/presentation/backend_picker_tile.dart'; import '../../chat/presentation/expert_picker_tile.dart'; @@ -100,6 +105,20 @@ class SettingsPage extends ConsumerWidget { onTap: () => _confirmDeleteAll(context, ref), ), const Divider(height: 1), + ListTile( + leading: Icon( + Icons.restore, + color: Theme.of(context).colorScheme.error, + ), + title: Text( + l10n.settingsResetAll, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + onTap: () => _confirmResetAll(context, ref), + ), + const Divider(height: 1), ], ), ); @@ -132,4 +151,57 @@ class SettingsPage extends ConsumerWidget { await ref.read(conversationsProvider.notifier).deleteAll(); } } + + Future _confirmResetAll(BuildContext context, WidgetRef ref) async { + final l10n = AppLocalizations.of(context); + final confirmed = await showDialog( + context: context, + useRootNavigator: true, + builder: (ctx) => AlertDialog( + title: Text(l10n.settingsResetAll), + content: Text(l10n.settingsResetAllConfirmation), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: Text( + l10n.settingsResetAllButton, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ), + ], + ), + ); + if (confirmed != true) return; + + // 1. Read installed model via provider before clearing preferences. + final storage = + await ref.read(chatModelPreferenceStorageProvider.future); + final installedModel = storage.readInstalled(); + + // 2. Clear all shared preferences. + final prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + + // 3. Uninstall downloaded model if one exists. + if (installedModel != null) { + try { + await FlutterGemma.uninstallModel(installedModel.fileName); + } catch (_) { + // Best-effort — model file may already be gone. + } + } + + // 4. Close and delete SQLite database. + final db = await ref.read(chatDatabaseProvider.future); + await db.close(); + final dbPath = join(await getDatabasesPath(), 'cookmate_chat.db'); + await deleteDatabase(dbPath); + + // 5. Close the app. + await SystemNavigator.pop(); + } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 22effc6..4912a3e 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -151,5 +151,15 @@ "settingsPerformanceTitle": "Leistungsüberwachung", "settingsPerformanceDescription": "Anonyme Leistungsdaten sammeln, um die App zu verbessern", - "settingsPerformanceChangeFailureSnackbar": "Leistungsüberwachung-Einstellung konnte nicht geändert werden. Bitte versuchen Sie es erneut." + "settingsPerformanceChangeFailureSnackbar": "Leistungsüberwachung-Einstellung konnte nicht geändert werden. Bitte versuchen Sie es erneut.", + + "chatModelDownloadNoConnectionTitle": "Keine Internetverbindung", + "chatModelDownloadNoConnectionBody": "Verbinde dich mit dem Internet, um das KI-Modell herunterzuladen.", + "chatModelDownloadMobileDataTitle": "Großer Download", + "chatModelDownloadMobileDataBody": "Der Download des KI-Modells ist groß. Eine WLAN-Verbindung wird empfohlen.", + "chatModelDownloadContinue": "Weiter", + + "settingsResetAll": "Alle Einstellungen zurücksetzen", + "settingsResetAllConfirmation": "Alle Einstellungen zurücksetzen? Die App wird geschlossen und alle Daten werden gelöscht. Dies kann nicht rückgängig gemacht werden.", + "settingsResetAllButton": "Zurücksetzen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e2299c1..5ee2ac7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -461,5 +461,29 @@ "@settingsPerformanceDescription": { "description": "Subtitle explaining what performance monitoring does." }, "settingsPerformanceChangeFailureSnackbar": "Couldn't change performance monitoring setting. Please try again.", - "@settingsPerformanceChangeFailureSnackbar": { "description": "Shown when persisting the performance monitoring toggle fails." } + "@settingsPerformanceChangeFailureSnackbar": { "description": "Shown when persisting the performance monitoring toggle fails." }, + + "chatModelDownloadNoConnectionTitle": "No internet connection", + "@chatModelDownloadNoConnectionTitle": { "description": "Dialog title when there is no internet connection before model download." }, + + "chatModelDownloadNoConnectionBody": "Connect to the internet to download the AI model.", + "@chatModelDownloadNoConnectionBody": { "description": "Dialog body when there is no internet connection before model download." }, + + "chatModelDownloadMobileDataTitle": "Large download", + "@chatModelDownloadMobileDataTitle": { "description": "Dialog title warning about large download on mobile data." }, + + "chatModelDownloadMobileDataBody": "The AI model download is large. A WiFi connection is recommended.", + "@chatModelDownloadMobileDataBody": { "description": "Dialog body warning about large download on mobile data." }, + + "chatModelDownloadContinue": "Continue", + "@chatModelDownloadContinue": { "description": "Button label to proceed with download on mobile data." }, + + "settingsResetAll": "Reset all settings", + "@settingsResetAll": { "description": "Label for the reset all settings action in settings." }, + + "settingsResetAllConfirmation": "Reset all settings? The app will close and all data will be deleted. This cannot be undone.", + "@settingsResetAllConfirmation": { "description": "Confirmation prompt before resetting all settings." }, + + "settingsResetAllButton": "Reset", + "@settingsResetAllButton": { "description": "Button label to confirm reset all settings." } } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index ac462ce..c33dc1a 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -151,5 +151,15 @@ "settingsPerformanceTitle": "Monitoreo de rendimiento", "settingsPerformanceDescription": "Recopilar datos de rendimiento anónimos para mejorar la aplicación", - "settingsPerformanceChangeFailureSnackbar": "No se pudo cambiar la configuración de monitoreo de rendimiento. Inténtelo de nuevo." + "settingsPerformanceChangeFailureSnackbar": "No se pudo cambiar la configuración de monitoreo de rendimiento. Inténtelo de nuevo.", + + "chatModelDownloadNoConnectionTitle": "Sin conexión a internet", + "chatModelDownloadNoConnectionBody": "Conéctate a internet para descargar el modelo de IA.", + "chatModelDownloadMobileDataTitle": "Descarga grande", + "chatModelDownloadMobileDataBody": "La descarga del modelo de IA es grande. Se recomienda usar una conexión WiFi.", + "chatModelDownloadContinue": "Continuar", + + "settingsResetAll": "Restablecer todos los ajustes", + "settingsResetAllConfirmation": "¿Restablecer todos los ajustes? La aplicación se cerrará y todos los datos serán eliminados. Esta acción no se puede deshacer.", + "settingsResetAllButton": "Restablecer" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index ad2c708..809f1a9 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -151,5 +151,15 @@ "settingsPerformanceTitle": "Rapport de performance", "settingsPerformanceDescription": "Collecter des données de performance anonymes pour améliorer l'application", - "settingsPerformanceChangeFailureSnackbar": "Impossible de modifier le paramètre de performance. Veuillez réessayer." + "settingsPerformanceChangeFailureSnackbar": "Impossible de modifier le paramètre de performance. Veuillez réessayer.", + + "chatModelDownloadNoConnectionTitle": "Pas de connexion internet", + "chatModelDownloadNoConnectionBody": "Connectez-vous à internet pour télécharger le modèle IA.", + "chatModelDownloadMobileDataTitle": "Téléchargement volumineux", + "chatModelDownloadMobileDataBody": "Le téléchargement du modèle IA est volumineux. Une connexion WiFi est recommandée.", + "chatModelDownloadContinue": "Continuer", + + "settingsResetAll": "Réinitialiser tous les réglages", + "settingsResetAllConfirmation": "Réinitialiser tous les réglages ? L'application se fermera et toutes les données seront supprimées. Cette action est irréversible.", + "settingsResetAllButton": "Réinitialiser" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb new file mode 100644 index 0000000..88db260 --- /dev/null +++ b/lib/l10n/app_it.arb @@ -0,0 +1,165 @@ +{ + "@@locale": "it", + "appTitle": "Cookmate", + "chatConversationsTitle": "Conversazioni", + "chatNewConversation": "Nuova conversazione", + "chatDeleteConversation": "Elimina conversazione", + "chatDeleteConfirmation": "Eliminare questa conversazione?", + "chatInputHint": "Scrivi un messaggio\u2026", + "chatEmptyState": "Nessuna conversazione. Tocca + per iniziare!", + "chatModelDownloadTitle": "Download del modello IA\u2026", + "chatModelDownloadProgress": "{progress}% completato", + "chatModelDownloadError": "Download fallito. Controlla la connessione e riprova.", + "chatModelDownloadRetry": "Riprova", + "settingsTitle": "Impostazioni", + "settingsSectionAi": "IA", + "settingsSectionGeneral": "Generali", + "settingsLanguageTitle": "Lingua", + "settingsLanguageFollowSystem": "Segui il sistema ({language})", + "settingsLanguageDialogTitle": "Scegli la lingua", + "settingsLanguageOptionSystem": "Segui il sistema", + "settingsLanguageChangeFailureSnackbar": "Impossibile cambiare la lingua. Riprova.", + "settingsThemeTitle": "Tema", + "settingsThemeDialogTitle": "Scegli il tema", + "settingsThemeOptionDark": "Scuro", + "settingsThemeOptionStandard": "Standard", + "settingsThemeOptionPink": "Rosa", + "settingsThemeOptionMatrix": "Matrix", + "settingsThemeChangeFailureSnackbar": "Impossibile cambiare il tema. Riprova.", + "settingsModelTitle": "Modello", + "settingsModelDialogTitle": "Scegli il modello IA", + "settingsModelChangeFailureSnackbar": "Impossibile cambiare il modello. Riprova.", + "settingsBackendTitle": "Acceleratore", + "settingsBackendDialogTitle": "Scegli il backend di inferenza", + "settingsBackendOptionGpu": "GPU (pi\u00f9 veloce, richiede un dispositivo compatibile)", + "settingsBackendOptionCpu": "CPU (pi\u00f9 lento, funziona ovunque)", + "settingsBackendChangeFailureSnackbar": "Impossibile cambiare il backend. Riprova.", + "settingsReasoningTitle": "Ragionamento", + "settingsReasoningSubtitleOn": "Attivato", + "settingsReasoningSubtitleOff": "Disattivato", + "settingsExpertTitle": "Esperto", + "settingsExpertDialogTitle": "Impostazioni esperto", + "settingsExpertMaxTokens": "Token massimi", + "settingsExpertMaxTokensInfo": "Numero massimo di token che il modello pu\u00f2 generare in una singola risposta. Valori pi\u00f9 alti consentono risposte pi\u00f9 lunghe ma utilizzano pi\u00f9 memoria.", + "settingsExpertTopK": "Top-K", + "settingsExpertTopKInfo": "Limita il modello alle K parole pi\u00f9 probabili. Valori bassi producono risposte pi\u00f9 precise; valori alti le rendono pi\u00f9 creative.", + "settingsExpertTopP": "Top-P", + "settingsExpertTopPInfo": "Campionamento a nucleo: il modello considera solo il pi\u00f9 piccolo insieme di parole la cui probabilit\u00e0 combinata supera P. Valori bassi producono un testo pi\u00f9 prevedibile.", + "settingsExpertTemperature": "Temperatura", + "settingsExpertTemperatureInfo": "Controlla la casualit\u00e0. Valori bassi (vicini a 0) producono risposte deterministiche e precise. Valori alti rendono il modello pi\u00f9 creativo e sorprendente.", + "settingsExpertTokenBuffer": "Token buffer", + "settingsExpertTokenBufferInfo": "Spazio riservato per la gestione del contesto. Valori elevati attivano la riduzione della cronologia prima, prevenendo la degenerazione nelle conversazioni lunghe. Intervallo: 256\u20134096.", + "settingsExpertReset": "Ripristina", + "settingsExpertSubtitle": "Token: {maxTokens} \u00b7 Temp: {temperature}", + "settingsExpertChangeFailureSnackbar": "Impossibile salvare le impostazioni esperto. Riprova.", + "settingsReasoningChangeFailureSnackbar": "Impossibile cambiare l'impostazione di ragionamento. Riprova.", + "chatTimeJustNow": "adesso", + "chatTimeMinutesAgo": "{minutes} min fa", + "chatTimeHoursAgo": "{hours} h fa", + "chatModelErrorBanner": "Errore modello: {error}", + "chatModelErrorRetry": "Riprova", + "chatModelLoading": "Il modello si sta caricando, attendere\u2026", + "chatAiInfoTitle": "Impostazioni IA", + "chatAiInfoModel": "Modello", + "chatAiInfoAccelerator": "Acceleratore", + "chatAiInfoReasoning": "Ragionamento", + "chatAiInfoMaxTokens": "Token massimi", + "chatAiInfoTemperature": "Temperatura", + "chatAiInfoTopK": "Top-K", + "chatAiInfoTopP": "Top-P", + "chatAiInfoTokenBuffer": "Token buffer", + "chatAiInfoClose": "Chiudi", + "chatThinkingLabel": "Sto pensando\u2026", + "cancel": "Annulla", + "delete": "Elimina", + "ok": "OK", + "homeTabChat": "Chat", + "homeTabSettings": "Impostazioni", + "splashTitle": "CookMate", + "splashDescription": "Crea le tue ricette Thermomix con l'assistente CookMate.", + + "chatAttachPhoto": "Foto", + "chatAttachGallery": "Galleria", + "chatAttachAudio": "Registra audio", + "chatRecordingInProgress": "Registrazione\u2026", + "chatImageCaption": "Immagine", + "chatAudioCaption": "Messaggio audio", + "chatAudioAttached": "Audio allegato", + "chatImageAttached": "Immagine allegata", + "chatMediaPermissionDenied": "Permesso negato. Consenti l'accesso nelle Impostazioni.", + "chatVisionUnavailable": "L'analisi delle immagini non \u00e8 disponibile su questo dispositivo.", + "chatImagePrompt": "Descrivi questa immagine e aiutami con questa ricetta.", + "chatAudioPrompt": "Trascrivi e rispondi a questo messaggio audio.", + "chatUserDisplayName": "Tu", + + "settingsSectionRecipe": "Ricetta", + "settingsTmVersionTitle": "Versione TM", + "settingsTmVersionDialogTitle": "Scegli la versione Thermomix", + "settingsTmVersionOptionTm5": "TM5", + "settingsTmVersionOptionTm6": "TM6", + "settingsTmVersionOptionTm7": "TM7", + "settingsUnitSystemTitle": "Sistema di unit\u00e0", + "settingsUnitSystemDialogTitle": "Scegli il sistema di unit\u00e0", + "settingsUnitSystemOptionMetric": "Metrico (g, ml, \u00b0C)", + "settingsUnitSystemOptionImperial": "Imperiale (oz, cups, \u00b0F)", + "settingsPortionsTitle": "Porzioni", + "settingsPortionsDialogTitle": "Scegli il numero di porzioni", + "settingsPortionsValue": "{count} porzioni", + "settingsLevelTitle": "Livello", + "settingsLevelDialogTitle": "Scegli il livello della ricetta", + "settingsLevelOptionBeginner": "Principiante", + "settingsLevelOptionIntermediate": "Intermedio", + "settingsLevelOptionAdvanced": "Avanzato", + "settingsLevelOptionAllLevels": "Tutti i livelli", + "settingsDietaryRestrictionsTitle": "Restrizioni alimentari", + "settingsDietaryRestrictionsDialogTitle": "Restrizioni alimentari", + "settingsDietaryRestrictionsHint": "es. senza glutine, vegetariano, senza frutta a guscio\u2026", + "settingsDietaryRestrictionsNone": "Nessuna", + "settingsRecipeChangeFailureSnackbar": "Impossibile salvare le impostazioni della ricetta. Riprova.", + "chatInfoTabRecipe": "Ricetta", + "chatInfoTabSkills": "Skills", + "chatInfoTabAi": "IA", + "chatRecipeInfoTmVersion": "Versione TM", + "chatRecipeInfoUnitSystem": "Sistema di unit\u00e0", + "chatRecipeInfoPortions": "Porzioni", + "chatRecipeInfoLevel": "Livello", + "chatRecipeInfoDietaryRestrictions": "Restrizioni alimentari", + "chatRecipeInfoLanguage": "Lingua", + "chatRenameConversation": "Rinomina conversazione", + "chatRenameHint": "Nome della conversazione", + "settingsSectionActions": "Azioni", + "settingsDeleteAllConversations": "Elimina tutte le conversazioni", + "settingsDeleteAllConversationsConfirmation": "Eliminare tutte le conversazioni? Questa azione non pu\u00f2 essere annullata.", + + "settingsSectionSkills": "Skills", + + "settingsSectionCookidoo": "Cookidoo", + "settingsCookidooEmailTitle": "Email", + "settingsCookidooEmailHint": "Email dell'account Cookidoo", + "settingsCookidooPasswordTitle": "Password", + "settingsCookidooPasswordHint": "Password dell'account Cookidoo", + "settingsCookidooTest": "Testa", + "settingsCookidooTestSuccess": "Connessione riuscita!", + "settingsCookidooTestFailure": "Connessione fallita. Controlla le tue credenziali.", + "settingsCookidooNotConfigured": "Non configurato", + "settingsCookidooChangeFailureSnackbar": "Impossibile salvare le credenziali Cookidoo. Riprova.", + + "settingsSectionObservability": "Osservabilit\u00e0", + "settingsCrashlyticsTitle": "Segnalazione errori", + "settingsCrashlyticsDescription": "Invia segnalazioni anonime di arresti anomali per migliorare l'app", + "settingsCrashlyticsChangeFailureSnackbar": "Impossibile modificare l'impostazione di segnalazione errori. Riprova.", + + "settingsPerformanceTitle": "Monitoraggio prestazioni", + "settingsPerformanceDescription": "Raccogliere dati anonimi sulle prestazioni per migliorare l'app", + "settingsPerformanceChangeFailureSnackbar": "Impossibile modificare l'impostazione di monitoraggio prestazioni. Riprova.", + + "chatModelDownloadNoConnectionTitle": "Nessuna connessione internet", + "chatModelDownloadNoConnectionBody": "Connettiti a internet per scaricare il modello IA.", + "chatModelDownloadMobileDataTitle": "Download di grandi dimensioni", + "chatModelDownloadMobileDataBody": "Il download del modello IA \u00e8 di grandi dimensioni. Si consiglia una connessione WiFi.", + "chatModelDownloadContinue": "Continua", + + "settingsResetAll": "Ripristina tutte le impostazioni", + "settingsResetAllConfirmation": "Ripristinare tutte le impostazioni? L'app si chiuderà e tutti i dati verranno eliminati. Questa azione non può essere annullata.", + "settingsResetAllButton": "Ripristina" +} diff --git a/pubspec.lock b/pubspec.lock index 566331f..8f12bde 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,6 +105,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" + url: "https://pub.dev" + source: hosted + version: "2.1.0" cross_cache: dependency: transitive description: @@ -145,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" diffutil_dart: dependency: transitive description: @@ -749,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" objective_c: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 58d0419..b1e58b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: firebase_core: ^3.13.0 firebase_crashlytics: ^4.3.5 firebase_performance: ^0.10.0+12 + connectivity_plus: ^6.1.4 dev_dependencies: flutter_test: