diff --git a/mobile/android/app/src/main/kotlin/com/forkfolio/mobile/MainActivity.kt b/mobile/android/app/src/main/kotlin/com/forkfolio/mobile/MainActivity.kt index 31d7d73..467d9cd 100644 --- a/mobile/android/app/src/main/kotlin/com/forkfolio/mobile/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/com/forkfolio/mobile/MainActivity.kt @@ -1,6 +1,33 @@ package com.forkfolio.mobile +import android.content.Intent import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel class MainActivity: FlutterActivity() { + private val CHANNEL = "com.forkfolio.mobile/native_share" + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + if (call.method == "shareText") { + val text = call.argument("text") ?: "" + val subject = call.argument("subject") ?: "" + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + if (subject.isNotEmpty()) { + putExtra(Intent.EXTRA_SUBJECT, subject) + } + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, subject.ifEmpty { "Share Recipe" }) + startActivity(shareIntent) + result.success(true) + } else { + result.notImplemented() + } + } + } } diff --git a/mobile/lib/core/api/api_client.dart b/mobile/lib/core/api/api_client.dart index 6aacb52..2074c61 100644 --- a/mobile/lib/core/api/api_client.dart +++ b/mobile/lib/core/api/api_client.dart @@ -164,6 +164,31 @@ class ApiClient { } } + // 4b. AI Polish Recipe with Gemini + Future> aiPolishRecipe({ + String? title, + String? rawCaption, + List? ingredients, + List? instructions, + }) async { + try { + final response = await _dio.post( + '/api/recipes/ai-polish', + data: { + 'title': title ?? '', + 'rawCaption': rawCaption ?? '', + 'ingredients': ingredients ?? [], + 'instructions': instructions ?? [], + }, + ); + return response.data as Map; + } on DioException catch (e) { + final err = e.error; + if (err is ApiException) throw err; + throw ApiException(e.message ?? 'AI Polish failed'); + } + } + // 5. Save or Update Recipe Future saveRecipe(Recipe recipe) async { try { diff --git a/mobile/lib/features/editor/recipe_edit_screen.dart b/mobile/lib/features/editor/recipe_edit_screen.dart new file mode 100644 index 0000000..e6a08fa --- /dev/null +++ b/mobile/lib/features/editor/recipe_edit_screen.dart @@ -0,0 +1,408 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/constants/app_theme.dart'; +import '../../core/models/recipe.dart'; +import '../../core/providers.dart'; + +class RecipeEditScreen extends ConsumerStatefulWidget { + final Recipe? recipe; + + const RecipeEditScreen({super.key, this.recipe}); + + @override + ConsumerState createState() => _RecipeEditScreenState(); +} + +class _RecipeEditScreenState extends ConsumerState { + final _formKey = GlobalKey(); + + late TextEditingController _titleController; + late TextEditingController _descController; + late TextEditingController _servingsController; + late TextEditingController _prepTimeController; + late TextEditingController _cookTimeController; + late TextEditingController _sourceUrlController; + late TextEditingController _imageUrlController; + late TextEditingController _ingredientsController; + late TextEditingController _instructionsController; + + bool _isSaving = false; + bool _isPolishing = false; + + @override + void initState() { + super.initState(); + final r = widget.recipe; + _titleController = TextEditingController(text: r?.title ?? ''); + _descController = TextEditingController(text: r?.description ?? ''); + _servingsController = TextEditingController(text: r?.servings ?? ''); + _prepTimeController = TextEditingController(text: r?.prepTime ?? ''); + _cookTimeController = TextEditingController(text: r?.cookTime ?? ''); + _sourceUrlController = TextEditingController(text: r?.sourceUrl ?? ''); + _imageUrlController = TextEditingController(text: r?.image ?? ''); + _ingredientsController = TextEditingController(text: r?.ingredients.join('\n') ?? ''); + _instructionsController = TextEditingController(text: r?.instructions.join('\n') ?? ''); + + _prepTimeController.addListener(_updateTimes); + _cookTimeController.addListener(_updateTimes); + } + + void _updateTimes() { + setState(() {}); + } + + @override + void dispose() { + _titleController.dispose(); + _descController.dispose(); + _servingsController.dispose(); + _prepTimeController.dispose(); + _cookTimeController.dispose(); + _sourceUrlController.dispose(); + _imageUrlController.dispose(); + _ingredientsController.dispose(); + _instructionsController.dispose(); + super.dispose(); + } + + String get _calculatedTotal { + return RecipeTimeUtils.calculateTotalTime( + prepTime: _prepTimeController.text.trim(), + cookTime: _cookTimeController.text.trim(), + totalTime: widget.recipe?.totalTime, + ); + } + + Future _handleAiPolish() async { + final title = _titleController.text.trim(); + final rawIng = _ingredientsController.text.trim(); + final rawInst = _instructionsController.text.trim(); + + if (rawIng.isEmpty && rawInst.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please add ingredients or instructions first to polish with AI.')), + ); + return; + } + + setState(() => _isPolishing = true); + try { + final api = ref.read(apiClientProvider); + final result = await api.aiPolishRecipe( + title: title, + ingredients: rawIng.split('\n').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(), + instructions: rawInst.split('\n').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(), + ); + + if (mounted) { + if (result['title'] != null && result['title'].toString().isNotEmpty) { + _titleController.text = result['title'].toString(); + } + if (result['description'] != null && result['description'].toString().isNotEmpty) { + _descController.text = result['description'].toString(); + } + if (result['servings'] != null && result['servings'].toString().isNotEmpty) { + _servingsController.text = result['servings'].toString(); + } + if (result['prepTime'] != null && result['prepTime'].toString().isNotEmpty) { + _prepTimeController.text = result['prepTime'].toString(); + } + if (result['cookTime'] != null && result['cookTime'].toString().isNotEmpty) { + _cookTimeController.text = result['cookTime'].toString(); + } + if (result['ingredients'] is List) { + final list = (result['ingredients'] as List).map((e) => e.toString()).toList(); + _ingredientsController.text = list.join('\n'); + } + if (result['instructions'] is List) { + final list = (result['instructions'] as List).map((e) => e.toString()).toList(); + _instructionsController.text = list.join('\n'); + } + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + backgroundColor: AppTheme.secondary, + content: Row( + children: [ + Icon(Icons.auto_awesome, color: Colors.white, size: 20), + SizedBox(width: 10), + Text('Recipe polished and organized by Gemini AI!'), + ], + ), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: AppTheme.danger, + content: Text('AI Polish failed: $e'), + ), + ); + } + } finally { + if (mounted) setState(() => _isPolishing = false); + } + } + + Future _handleSave() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isSaving = true); + final isNew = widget.recipe == null; + + final prep = _prepTimeController.text.trim(); + final cook = _cookTimeController.text.trim(); + final total = RecipeTimeUtils.calculateTotalTime( + prepTime: prep, + cookTime: cook, + totalTime: widget.recipe?.totalTime, + ); + + final ingList = _ingredientsController.text + .split('\n') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + final instList = _instructionsController.text + .split('\n') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + final updated = Recipe( + id: widget.recipe?.id, + title: _titleController.text.trim(), + description: _descController.text.trim(), + servings: _servingsController.text.trim(), + prepTime: prep, + cookTime: cook, + totalTime: total, + sourceUrl: _sourceUrlController.text.trim(), + image: _imageUrlController.text.trim().isNotEmpty + ? _imageUrlController.text.trim() + : widget.recipe?.image, + images: widget.recipe?.images ?? [], + ingredients: ingList, + instructions: instList, + shareToken: widget.recipe?.shareToken, + ); + + try { + await ref.read(recipeListProvider.notifier).addOrUpdate(updated); + if (mounted) { + Navigator.pop(context, true); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: AppTheme.secondary, + content: Text(isNew ? 'Created "${updated.title}"' : 'Updated "${updated.title}"'), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: AppTheme.danger, + content: Text('Failed to save recipe: $e'), + ), + ); + } + } finally { + if (mounted) setState(() => _isSaving = false); + } + } + + @override + Widget build(BuildContext context) { + final isNew = widget.recipe == null; + + return Scaffold( + appBar: AppBar( + title: Text(isNew ? 'New Recipe' : 'Edit Recipe'), + actions: [ + if (_isPolishing) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.secondary))), + ) + else + TextButton.icon( + onPressed: _handleAiPolish, + icon: const Icon(Icons.auto_awesome, color: AppTheme.secondary, size: 18), + label: const Text('AI Polish', style: TextStyle(color: AppTheme.secondary, fontWeight: FontWeight.w700)), + ), + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.only(right: 12), + child: _isSaving + ? const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.primary))) + : ElevatedButton( + onPressed: _handleSave, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + minimumSize: const Size(60, 36), + ), + child: const Text('Save'), + ), + ), + ], + ), + body: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + // Title Field + TextFormField( + controller: _titleController, + decoration: const InputDecoration( + labelText: 'Recipe Title *', + hintText: 'e.g. Creamy Tuscan Garlic Chicken', + prefixIcon: Icon(Icons.restaurant_rounded), + ), + validator: (val) => (val == null || val.trim().isEmpty) ? 'Please enter a recipe title' : null, + ), + const SizedBox(height: 14), + + // Description Field + TextFormField( + controller: _descController, + maxLines: 2, + decoration: const InputDecoration( + labelText: 'Description', + hintText: 'A short overview or tasting notes', + prefixIcon: Icon(Icons.description_outlined), + ), + ), + const SizedBox(height: 14), + + // Servings, Prep Time, Cook Time Row + Row( + children: [ + Expanded( + child: TextFormField( + controller: _servingsController, + decoration: const InputDecoration( + labelText: 'Servings', + hintText: 'e.g. 4', + isDense: true, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: TextFormField( + controller: _prepTimeController, + decoration: const InputDecoration( + labelText: 'Prep Time', + hintText: '15 mins', + isDense: true, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: TextFormField( + controller: _cookTimeController, + decoration: const InputDecoration( + labelText: 'Cook Time', + hintText: '30 mins', + isDense: true, + ), + ), + ), + ], + ), + + // Total Time live preview badge + if (_calculatedTotal.isNotEmpty) ...[ + const SizedBox(height: 8), + Row( + children: [ + const Icon(Icons.timer_rounded, size: 14, color: AppTheme.primary), + const SizedBox(width: 6), + Text( + 'Calculated Total Time: $_calculatedTotal', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: AppTheme.primary), + ), + ], + ), + ], + const SizedBox(height: 14), + + // Source URL + TextFormField( + controller: _sourceUrlController, + decoration: const InputDecoration( + labelText: 'Source URL', + hintText: 'https://...', + prefixIcon: Icon(Icons.link_rounded), + isDense: true, + ), + ), + const SizedBox(height: 14), + + // Image URL + TextFormField( + controller: _imageUrlController, + decoration: const InputDecoration( + labelText: 'Cover Image URL', + hintText: 'https://...', + prefixIcon: Icon(Icons.image_outlined), + isDense: true, + ), + ), + const SizedBox(height: 20), + + // Ingredients Section + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Ingredients', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.primary)), + Text( + 'One per line', + style: TextStyle(fontSize: 12, color: AppTheme.textMuted.withOpacity(0.8)), + ), + ], + ), + const SizedBox(height: 8), + TextFormField( + controller: _ingredientsController, + maxLines: 8, + decoration: const InputDecoration( + hintText: '2 chicken breasts, diced\n1 cup heavy cream\n2 cloves garlic, minced\n1 cup baby spinach', + alignLabelWithHint: true, + ), + ), + const SizedBox(height: 20), + + // Instructions Section + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Instructions', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.primary)), + Text( + 'One step per line', + style: TextStyle(fontSize: 12, color: AppTheme.textMuted.withOpacity(0.8)), + ), + ], + ), + const SizedBox(height: 8), + TextFormField( + controller: _instructionsController, + maxLines: 8, + decoration: const InputDecoration( + hintText: 'Heat olive oil in a skillet over medium heat.\nAdd garlic and cook until fragrant.\nPour in heavy cream and simmer until thickened.', + alignLabelWithHint: true, + ), + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/features/library/recipes_screen.dart b/mobile/lib/features/library/recipes_screen.dart index 599950a..3b05130 100644 --- a/mobile/lib/features/library/recipes_screen.dart +++ b/mobile/lib/features/library/recipes_screen.dart @@ -1,10 +1,12 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/constants/app_theme.dart'; import '../../core/models/recipe.dart'; import '../../core/providers.dart'; import '../cook_mode/cook_mode_screen.dart'; +import '../editor/recipe_edit_screen.dart'; import '../scanner/scanner_screen.dart'; class RecipesScreen extends ConsumerStatefulWidget { @@ -47,12 +49,21 @@ class _RecipesScreenState extends ConsumerState { }, ), IconButton( - tooltip: 'Logout', - icon: const Icon(Icons.logout_rounded), - onPressed: () => ref.read(authProvider.notifier).logout(), + tooltip: 'Profile & Settings', + icon: const Icon(Icons.account_circle_outlined), + onPressed: _showProfileDialog, ), ], ), + floatingActionButton: FloatingActionButton.extended( + backgroundColor: AppTheme.primary, + foregroundColor: Colors.black, + icon: const Icon(Icons.add_rounded), + label: const Text('New Recipe', style: TextStyle(fontWeight: FontWeight.w700)), + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (_) => const RecipeEditScreen())); + }, + ), body: RefreshIndicator( color: AppTheme.primary, onRefresh: () => ref.read(recipeListProvider.notifier).loadRecipes(), @@ -278,17 +289,70 @@ class _RecipesScreenState extends ConsumerState { ], ), const SizedBox(height: 16), - SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton.icon( - onPressed: () { - Navigator.pop(ctx); - Navigator.push(context, MaterialPageRoute(builder: (_) => CookModeScreen(recipe: recipe))); - }, - icon: const Icon(Icons.outdoor_grill_rounded), - label: const Text('Start Cook Mode'), - ), + Row( + children: [ + Expanded( + child: SizedBox( + height: 48, + child: ElevatedButton.icon( + onPressed: () { + Navigator.pop(ctx); + Navigator.push(context, MaterialPageRoute(builder: (_) => CookModeScreen(recipe: recipe))); + }, + icon: const Icon(Icons.outdoor_grill_rounded), + label: const Text('Cook Mode'), + ), + ), + ), + const SizedBox(width: 8), + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + color: AppTheme.surfaceElevated, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppTheme.border), + ), + child: IconButton( + tooltip: 'Edit Recipe', + icon: const Icon(Icons.edit_outlined, color: AppTheme.secondary, size: 20), + onPressed: () { + Navigator.pop(ctx); + Navigator.push(context, MaterialPageRoute(builder: (_) => RecipeEditScreen(recipe: recipe))); + }, + ), + ), + const SizedBox(width: 8), + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + color: AppTheme.surfaceElevated, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppTheme.border), + ), + child: IconButton( + tooltip: 'Share Recipe', + icon: const Icon(Icons.share_rounded, color: AppTheme.primary, size: 20), + onPressed: () => _shareRecipe(recipe), + ), + ), + const SizedBox(width: 8), + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + color: AppTheme.surfaceElevated, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppTheme.border), + ), + child: IconButton( + tooltip: 'Delete Recipe', + icon: const Icon(Icons.delete_outline_rounded, color: AppTheme.danger, size: 20), + onPressed: () => _confirmDelete(ctx, recipe), + ), + ), + ], ), const SizedBox(height: 24), const Text('Ingredients', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.primary)), @@ -344,4 +408,180 @@ class _RecipesScreenState extends ConsumerState { ), ); } + + Future _shareRecipe(Recipe recipe) async { + final storage = ref.read(secureStorageProvider); + final serverUrl = await storage.getServerUrl() ?? ''; + final buffer = StringBuffer(); + buffer.writeln('šŸ³ ${recipe.title}'); + if (recipe.calculatedTotalTime.isNotEmpty) { + buffer.writeln('ā±ļø Total Time: ${recipe.calculatedTotalTime}'); + } + if (recipe.servings != null && recipe.servings!.isNotEmpty) { + buffer.writeln('šŸ‘„ Servings: ${recipe.servings}'); + } + if (recipe.shareToken != null && recipe.shareToken!.isNotEmpty && serverUrl.isNotEmpty) { + final cleanBase = serverUrl.endsWith('/') ? serverUrl.substring(0, serverUrl.length - 1) : serverUrl; + buffer.writeln('šŸ”— ForkFolio Link: $cleanBase/share/${recipe.shareToken}'); + } else if (recipe.sourceUrl != null && recipe.sourceUrl!.isNotEmpty) { + buffer.writeln('šŸ”— Source: ${recipe.sourceUrl}'); + } + + buffer.writeln('\nšŸ›’ Ingredients:'); + for (final ing in recipe.ingredients) { + buffer.writeln('• $ing'); + } + + buffer.writeln('\nšŸ“‹ Instructions:'); + for (var i = 0; i < recipe.instructions.length; i++) { + buffer.writeln('${i + 1}. ${recipe.instructions[i]}'); + } + + try { + const platform = MethodChannel('com.forkfolio.mobile/native_share'); + await platform.invokeMethod('shareText', { + 'text': buffer.toString(), + 'subject': recipe.title, + }); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Could not trigger share: $e')), + ); + } + } + } + + Future _confirmDelete(BuildContext sheetCtx, Recipe recipe) async { + final confirmed = await showDialog( + context: context, + builder: (dlgCtx) => AlertDialog( + backgroundColor: AppTheme.surfaceElevated, + title: const Text('Delete Recipe?'), + content: Text('Are you sure you want to delete "${recipe.title}"? This cannot be undone.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dlgCtx, false), + child: const Text('Cancel', style: TextStyle(color: AppTheme.textMuted)), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: AppTheme.danger), + onPressed: () => Navigator.pop(dlgCtx, true), + child: const Text('Delete', style: TextStyle(color: Colors.white)), + ), + ], + ), + ); + + if (confirmed == true && recipe.id != null) { + Navigator.pop(sheetCtx); + try { + await ref.read(recipeListProvider.notifier).remove(recipe.id!); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: AppTheme.surfaceElevated, + content: Text('Deleted "${recipe.title}"'), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: AppTheme.danger, + content: Text('Failed to delete recipe: $e'), + ), + ); + } + } + } + } + + void _showProfileDialog() async { + final user = ref.read(authProvider).user; + final storage = ref.read(secureStorageProvider); + final serverUrl = await storage.getServerUrl(); + final username = user?['username'] ?? 'User'; + final role = user?['isAdmin'] == true + ? 'Administrator' + : (user?['isRecipeManager'] == true ? 'Recipe Manager' : 'Member'); + + if (!mounted) return; + + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (ctx) => Container( + padding: const EdgeInsets.all(24), + decoration: const BoxDecoration( + color: AppTheme.surfaceElevated, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 24, + backgroundColor: AppTheme.primary.withOpacity(0.2), + child: Text( + username.isNotEmpty ? username[0].toUpperCase() : 'U', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: AppTheme.primary), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(username, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), + const SizedBox(height: 2), + Text(role, style: const TextStyle(fontSize: 13, color: AppTheme.secondary, fontWeight: FontWeight.w600)), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + const Divider(color: AppTheme.border), + const SizedBox(height: 12), + Row( + children: [ + const Icon(Icons.dns_rounded, size: 16, color: AppTheme.textMuted), + const SizedBox(width: 8), + Expanded( + child: Text( + serverUrl ?? 'Connected Server', + style: const TextStyle(fontSize: 13, color: AppTheme.textMuted), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 48, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.danger, + side: const BorderSide(color: AppTheme.danger), + ), + icon: const Icon(Icons.logout_rounded), + label: const Text('Log Out'), + onPressed: () { + Navigator.pop(ctx); + ref.read(authProvider.notifier).logout(); + }, + ), + ), + const SizedBox(height: 12), + ], + ), + ), + ); + } }