You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

283 line
9.7KB

  1. // lib/presentation/screens/ai_enhancement/ai_enhancement_screen.dart
  2. import 'dart:async';
  3. import 'dart:convert';
  4. import 'dart:io';
  5. import 'dart:typed_data';
  6. import 'package:flutter/material.dart';
  7. import 'package:image/image.dart' as img;
  8. import '../../../domain/app_filter.dart';
  9. import '../../../domain/catalogs/filter_catalog.dart';
  10. import '../../../repositories/ai_repository.dart';
  11. // --- AJOUT NÉCESSAIRE POUR LA NAVIGATION ---
  12. import '../../../routes/app_routes.dart';
  13. // --- CLASSE D'ARGUMENTS (INCHANGÉE) ---
  14. class AiEnhancementScreenArguments {
  15. AiEnhancementScreenArguments({
  16. required this.image,
  17. required this.initialPrompt,
  18. required this.suggestedFilterIds,
  19. required this.aiRepository,
  20. });
  21. final File image;
  22. final String initialPrompt;
  23. final List<String> suggestedFilterIds;
  24. final AiRepository aiRepository;
  25. }
  26. // --- ÉNUMÉRATIONS (INCHANGÉES) ---
  27. enum GenerationState { idle, generating, done, error }
  28. enum ImageGenerationEngine { stableDiffusion, gemini }
  29. class AiEnhancementScreen extends StatefulWidget {
  30. const AiEnhancementScreen({required this.arguments, super.key});
  31. final AiEnhancementScreenArguments arguments;
  32. @override
  33. State<AiEnhancementScreen> createState() => _AiEnhancementScreenState();
  34. }
  35. class _AiEnhancementScreenState extends State<AiEnhancementScreen> {
  36. // --- GESTION D'ÉTAT ---
  37. GenerationState _generationState = GenerationState.idle;
  38. ImageGenerationEngine? _selectedEngine;
  39. final List<Uint8List> _generatedImagesData = [];
  40. StreamSubscription? _imageStreamSubscription;
  41. // --- NOUVEL ÉTAT POUR GÉRER LA VISIBILITÉ DU PROMPT ---
  42. bool _isPromptVisible = false;
  43. // --- DONNÉES DE L'ÉCRAN ---
  44. late final TextEditingController _promptController;
  45. late final List<ImageFilter> _preselectedFilters;
  46. @override
  47. void initState() {
  48. super.initState();
  49. _promptController = TextEditingController(text: widget.arguments.initialPrompt);
  50. _preselectedFilters = availableFilters
  51. .where((filter) => widget.arguments.suggestedFilterIds.contains(filter.id))
  52. .toList();
  53. }
  54. @override
  55. void dispose() {
  56. _imageStreamSubscription?.cancel();
  57. _promptController.dispose();
  58. super.dispose();
  59. }
  60. /// Lance la génération d'images avec le moteur spécifié.
  61. Future<void> _startGeneration(ImageGenerationEngine engine) async {
  62. if (_generationState == GenerationState.generating) return;
  63. setState(() {
  64. _generatedImagesData.clear();
  65. _generationState = GenerationState.generating;
  66. _selectedEngine = engine;
  67. });
  68. try {
  69. final imageBytes = await widget.arguments.image.readAsBytes();
  70. final imageBase64 = base64Encode(imageBytes);
  71. // --- DÉBUT DE LA CORRECTION ---
  72. // 1. Décoder l'image pour obtenir ses dimensions
  73. final originalImage = img.decodeImage(imageBytes);
  74. if (originalImage == null) {
  75. throw Exception("Impossible de lire les dimensions de l'image.");
  76. }
  77. // 2. Récupérer la largeur et la hauteur
  78. final int originalWidth = originalImage.width;
  79. final int originalHeight = originalImage.height;
  80. // --- FIN DE LA CORRECTION ---
  81. await _imageStreamSubscription?.cancel();
  82. _imageStreamSubscription = widget.arguments.aiRepository.editImage(
  83. imageBase64,
  84. _promptController.text,
  85. originalWidth,
  86. originalHeight,
  87. filtersToApply: _preselectedFilters,
  88. ).listen(
  89. (receivedBase64Image) {
  90. if (!mounted) return;
  91. setState(() => _generatedImagesData.add(base64Decode(receivedBase64Image)));
  92. },
  93. onError: (error) {
  94. if (!mounted) return;
  95. setState(() => _generationState = GenerationState.error);
  96. ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erreur de génération : $error')));
  97. },
  98. onDone: () {
  99. if (!mounted) return;
  100. setState(() => _generationState = GenerationState.done);
  101. },
  102. );
  103. } catch (e) {
  104. if (!mounted) return;
  105. setState(() => _generationState = GenerationState.error);
  106. ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erreur : ${e.toString()}')));
  107. }
  108. }
  109. // --- NOUVELLE MÉTHODE POUR NAVIGUER VERS L'APERÇU ---
  110. void _navigateToPreview(int index) {
  111. if (index >= _generatedImagesData.length) return;
  112. final selectedImageData = _generatedImagesData[index];
  113. final imageBase64 = base64Encode(selectedImageData);
  114. Navigator.pushNamed(context, AppRoutes.imagePreview, arguments: imageBase64);
  115. }
  116. @override
  117. Widget build(BuildContext context) {
  118. return Scaffold(
  119. appBar: AppBar(title: const Text('2. Amélioration IA')),
  120. body: SingleChildScrollView(
  121. padding: const EdgeInsets.all(16.0),
  122. child: Column(
  123. crossAxisAlignment: CrossAxisAlignment.stretch,
  124. children: [
  125. _buildEngineChoiceSection(),
  126. const SizedBox(height: 24),
  127. // --- SECTION DU PROMPT MISE À JOUR ---
  128. _buildCollapsiblePromptSection(),
  129. const SizedBox(height: 24),
  130. Text('Résultats de la génération', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
  131. const SizedBox(height: 16),
  132. // --- GRILLE DES RÉSULTATS MISE À JOUR ---
  133. _buildGeneratedVariationsGrid(),
  134. ],
  135. ),
  136. ),
  137. );
  138. }
  139. Widget _buildEngineChoiceSection() {
  140. return Card(
  141. elevation: 2,
  142. child: Padding(
  143. padding: const EdgeInsets.all(16.0),
  144. child: Row(
  145. children: [
  146. ClipRRect(
  147. borderRadius: BorderRadius.circular(8.0),
  148. child: Image.file(
  149. widget.arguments.image,
  150. width: 100,
  151. height: 100,
  152. fit: BoxFit.cover,
  153. ),
  154. ),
  155. const SizedBox(width: 16),
  156. Expanded(
  157. child: Column(
  158. crossAxisAlignment: CrossAxisAlignment.stretch,
  159. children: [
  160. Text('Choisir le moteur IA', style: Theme.of(context).textTheme.titleMedium),
  161. const SizedBox(height: 12),
  162. FilledButton.icon(
  163. onPressed: _generationState == GenerationState.generating ? null : () => _startGeneration(ImageGenerationEngine.stableDiffusion),
  164. icon: const Icon(Icons.auto_awesome_outlined),
  165. label: const Text('Stable Diffusion'),
  166. ),
  167. const SizedBox(height: 8),
  168. OutlinedButton.icon(
  169. onPressed: _generationState == GenerationState.generating ? null : () => _startGeneration(ImageGenerationEngine.gemini),
  170. icon: const Icon(Icons.bubble_chart_outlined),
  171. label: const Text('Gemini'),
  172. ),
  173. ],
  174. ),
  175. ),
  176. ],
  177. ),
  178. ),
  179. );
  180. }
  181. /// --- NOUVELLE VERSION DE LA SECTION PROMPT ---
  182. /// Utilise un ExpansionTile pour être masquable/affichable.
  183. Widget _buildCollapsiblePromptSection() {
  184. return ExpansionTile(
  185. title: Text(
  186. 'Prompt de Guidage',
  187. style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
  188. ),
  189. subtitle: Text(
  190. _isPromptVisible ? 'Modifiez le prompt pour affiner le résultat' : 'Afficher pour modifier',
  191. style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey.shade600),
  192. ),
  193. onExpansionChanged: (isExpanded) {
  194. setState(() => _isPromptVisible = isExpanded);
  195. },
  196. children: [
  197. Padding(
  198. padding: const EdgeInsets.only(top: 8.0, bottom: 16.0),
  199. child: TextField(
  200. controller: _promptController,
  201. decoration: const InputDecoration(
  202. hintText: 'Décrivez l\'image que vous souhaitez obtenir...',
  203. border: OutlineInputBorder(),
  204. ),
  205. maxLines: 4,
  206. minLines: 2,
  207. ),
  208. ),
  209. ],
  210. );
  211. }
  212. /// --- NOUVELLE VERSION DE LA GRILLE DES RÉSULTATS ---
  213. /// Utilise un GestureDetector pour rendre chaque image cliquable.
  214. Widget _buildGeneratedVariationsGrid() {
  215. if (_generationState == GenerationState.idle) {
  216. return Container(
  217. height: 100,
  218. decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8)),
  219. child: const Center(child: Text("Choisissez un moteur pour commencer.")),
  220. );
  221. }
  222. if (_generationState == GenerationState.generating) {
  223. return const Padding(
  224. padding: EdgeInsets.all(32.0),
  225. child: Center(child: CircularProgressIndicator()),
  226. );
  227. }
  228. if (_generationState == GenerationState.done && _generatedImagesData.isEmpty) {
  229. return Container(
  230. height: 100,
  231. decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200)),
  232. child: const Center(child: Text("La génération n'a produit aucune image.", textAlign: TextAlign.center)),
  233. );
  234. }
  235. return GridView.builder(
  236. shrinkWrap: true,
  237. physics: const NeverScrollableScrollPhysics(),
  238. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  239. crossAxisCount: 2,
  240. crossAxisSpacing: 8,
  241. mainAxisSpacing: 8,
  242. ),
  243. itemCount: _generatedImagesData.length,
  244. itemBuilder: (context, index) {
  245. // ON ENTOURE L'IMAGE D'UN GESTUREDETECTOR
  246. return GestureDetector(
  247. onTap: () => _navigateToPreview(index),
  248. child: ClipRRect(
  249. borderRadius: BorderRadius.circular(8),
  250. child: Image.memory(_generatedImagesData[index], fit: BoxFit.cover),
  251. ),
  252. );
  253. },
  254. );
  255. }
  256. }