|
- import 'dart:async';
- import 'dart:convert';
- import 'dart:io';
- import 'dart:typed_data';
- import 'package:flutter/material.dart';import 'package:social_content_creator/routes/app_routes.dart';
- import 'package:social_content_creator/services/image_editing_service.dart'; // Contrat
- import 'package:social_content_creator/services/stable_diffusion_service.dart'; // Moteur 1
- import 'package:social_content_creator/services/gemini_service.dart'; // Moteur 2
- import 'package:image/image.dart' as img;
-
- // Énumération pour le choix du moteur d'IA
- enum ImageEngine { stableDiffusion, gemini }
-
- class AiEnhancementScreen extends StatefulWidget {
- final File image;
- final String prompt;
-
- const AiEnhancementScreen({
- super.key,
- required this.image,
- required this.prompt,
- });
-
- @override
- State<AiEnhancementScreen> createState() => _AiEnhancementScreenState();
- }
-
- enum GenerationState { idle, generating, done, error }
-
- class _AiEnhancementScreenState extends State<AiEnhancementScreen> {
- GenerationState _generationState = GenerationState.idle;
- final List<Uint8List> _generatedImagesData = [];
- StreamSubscription? _imageStreamSubscription;
-
- // --- NOUVEAUX ÉLÉMENTS ---
- // 1. Instancier les deux services dans une map
- final Map<ImageEngine, ImageEditingService> _services = {
- ImageEngine.stableDiffusion: StableDiffusionService(),
- ImageEngine.gemini: GeminiService(),
- };
-
- // 2. Garder en mémoire le moteur sélectionné (Stable Diffusion par défaut)
- ImageEngine _selectedEngine = ImageEngine.stableDiffusion;
- // --- FIN DES NOUVEAUX ÉLÉMENTS ---
-
- Future<void> _generateImageVariations() async {
- if (_generationState == GenerationState.generating) return;
-
- setState(() {
- _generatedImagesData.clear();
- _generationState = GenerationState.generating;
- });
-
- try {
- // Préparation de l'image (logique inchangée)
- final imageBytes = await widget.image.readAsBytes();
- final originalImage = img.decodeImage(imageBytes);
- if (originalImage == null) throw Exception("Impossible de décoder l'image.");
-
- final resizedImage = img.copyResize(originalImage, width: 1024);
- final resizedImageBytes = img.encodeJpg(resizedImage, quality: 90);
- final imageBase64 = base64Encode(resizedImageBytes);
-
- await _imageStreamSubscription?.cancel();
-
- // --- UTILISATION DU SERVICE SÉLECTIONNÉ ---
- // On récupère le bon service (Stable Diffusion ou Gemini) depuis la map
- final activeService = _services[_selectedEngine]!;
-
- _imageStreamSubscription = activeService.editImage(
- imageBase64,
- widget.prompt,
- resizedImage.width,
- resizedImage.height,
- // On demande 3 images à Stable Diffusion, Gemini gèrera ce paramètre
- numberOfImages: _selectedEngine == ImageEngine.stableDiffusion ? 3 : 1,
- ).listen(
- (receivedBase64Image) {
- setState(() => _generatedImagesData.add(base64Decode(receivedBase64Image)));
- },
- onError: (error) {
- if (!mounted) return;
- setState(() => _generationState = GenerationState.error);
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erreur de génération : $error")));
- },
- onDone: () {
- if (!mounted) return;
- setState(() => _generationState = GenerationState.done);
- },
- );
-
- } catch (e) {
- if (!mounted) return;
- setState(() => _generationState = GenerationState.error);
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erreur : ${e.toString()}")));
- }
- }
-
- void _navigateToPreview(int index) {
- if (index >= _generatedImagesData.length) return;
- final selectedImageData = _generatedImagesData[index];
- final imageBase64 = base64Encode(selectedImageData);
- Navigator.pushNamed(context, AppRoutes.imagePreview, arguments: imageBase64);
- }
-
- @override
- void dispose() {
- _imageStreamSubscription?.cancel();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- const double bottomButtonHeight = 90.0;
-
- return Scaffold(
- appBar: AppBar(title: const Text("3. Choisir une variation")),
- body: Stack(
- children: [
- SingleChildScrollView(
- padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, bottomButtonHeight),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text("Image Originale", style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
- const SizedBox(height: 8),
- SizedBox(
- height: 300,
- width: double.infinity,
- child: ClipRRect(
- borderRadius: BorderRadius.circular(12.0),
- child: Image.file(widget.image, fit: BoxFit.cover),
- ),
- ),
- const SizedBox(height: 16),
-
- // --- AJOUT DU SÉLECTEUR DE MOTEUR D'IA ---
- Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.2))
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Text(
- "Moteur d'IA pour la variation",
- style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 12),
- SegmentedButton<ImageEngine>(
- showSelectedIcon: false,
- segments: const <ButtonSegment<ImageEngine>>[
- ButtonSegment<ImageEngine>(
- value: ImageEngine.stableDiffusion,
- label: Text('Stable Diffusion'),
- icon: Icon(Icons.auto_awesome_outlined),
- ),
- ButtonSegment<ImageEngine>(
- value: ImageEngine.gemini,
- label: Text('Gemini'),
- icon: Icon(Icons.bubble_chart_outlined),
- ),
- ],
- selected: {_selectedEngine},
- onSelectionChanged: (Set<ImageEngine> newSelection) {
- // On ne change de moteur que si la génération n'est pas en cours
- if (_generationState != GenerationState.generating) {
- setState(() {
- _selectedEngine = newSelection.first;
- });
- }
- },
- ),
- const SizedBox(height: 16),
- // On garde le prompt de guidage dans le même encart
- ExpansionTile(
- title: const Text("Voir le prompt de guidage"),
- initiallyExpanded: false,
- tilePadding: EdgeInsets.zero,
- childrenPadding: const EdgeInsets.only(top: 8),
- children: [
- Text(widget.prompt, style: const TextStyle(fontStyle: FontStyle.italic)),
- ],
- ),
- ],
- ),
- ),
- // --- FIN DE L'AJOUT ---
-
- const SizedBox(height: 24),
- Text("Variations (cliquez pour choisir)", style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
- const SizedBox(height: 16),
- _buildGeneratedVariationsGrid(),
- ],
- ),
- ),
- Positioned(
- bottom: 0,
- left: 0,
- right: 0,
- child: Container(
- color: Theme.of(context).scaffoldBackgroundColor,
- padding: const EdgeInsets.all(16.0),
- child: FilledButton.icon(
- icon: _generationState == GenerationState.generating ? const SizedBox.shrink() : const Icon(Icons.auto_awesome),
- label: Text(_generationState == GenerationState.generating ? 'Génération en cours...' : 'Générer à nouveau'),
- onPressed: _generationState == GenerationState.generating ? null : _generateImageVariations,
- style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
- ),
- ),
- ),
- ],
- ),
- );
- }
-
- // Le reste du fichier est inchangé (_buildGeneratedVariationsGrid)
- Widget _buildGeneratedVariationsGrid() {
- // Si la génération est terminée et qu'il n'y a aucune image (cas d'erreur silencieuse)
- if (_generationState == GenerationState.done && _generatedImagesData.isEmpty) {
- return Container(
- height: 100,
- decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200)),
- child: const Center(child: Text("La génération n'a produit aucune image.", textAlign: TextAlign.center)),
- );
- }
-
- if (_generationState == GenerationState.idle) {
- return Container(
- height: 100,
- decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade400, style: BorderStyle.solid)),
- child: const Center(child: Text("Cliquez sur 'Générer' pour commencer.")),
- );
- }
-
- // On affiche 3 slots, même si Gemini n'en remplit qu'un
- int displayCount = 3;
- if (_selectedEngine == ImageEngine.gemini && _generationState != GenerationState.generating) {
- displayCount = _generatedImagesData.length > 0 ? _generatedImagesData.length : 1;
- }
-
- return Row(
- mainAxisAlignment: MainAxisAlignment.start,
- children: List.generate(displayCount, (index) {
- bool hasImage = index < _generatedImagesData.length;
- return Expanded(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 4.0),
- child: AspectRatio(
- aspectRatio: 1.0,
- child: GestureDetector(
- onTap: hasImage ? () => _navigateToPreview(index) : null,
- child: hasImage
- ? ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.memory(_generatedImagesData[index], fit: BoxFit.cover))
- : Container(
- decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(8)),
- child: _generationState == GenerationState.generating ? const Center(child: CircularProgressIndicator()) : const SizedBox(),
- ),
- ),
- ),
- ),
- );
- }),
- );
- }
- }
|