| # Miscellaneous | |||||
| *.class | |||||
| *.log | |||||
| *.pyc | |||||
| *.swp | |||||
| .DS_Store | |||||
| .atom/ | |||||
| .buildlog/ | |||||
| .history | |||||
| .svn/ | |||||
| migrate_working_dir/ | |||||
| # IntelliJ related | |||||
| *.iml | |||||
| *.ipr | |||||
| *.iws | |||||
| .idea/ | |||||
| # The .vscode folder contains launch configuration and tasks you configure in | |||||
| # VS Code which you may wish to be included in version control, so this line | |||||
| # is commented out by default. | |||||
| #.vscode/ | |||||
| # Flutter/Dart/Pub related | |||||
| **/doc/api/ | |||||
| **/ios/Flutter/.last_build_id | |||||
| .dart_tool/ | |||||
| .flutter-plugins | |||||
| .flutter-plugins-dependencies | |||||
| .packages | |||||
| .pub-cache/ | |||||
| .pub/ | |||||
| /build/ | |||||
| # Symbolication related | |||||
| app.*.symbols | |||||
| # Obfuscation related | |||||
| app.*.map.json | |||||
| # Android Studio will place build artifacts here | |||||
| /android/app/debug | |||||
| /android/app/profile | |||||
| /android/app/release | |||||
| /android/app/.gradle/ | |||||
| /android/.gradle/ | |||||
| /android/local.properties | |||||
| /android/key.properties | |||||
| # iOS/macOS | |||||
| **/ios/**/*.mode1v3 | |||||
| **/ios/**/*.mode2v3 | |||||
| **/ios/**/*.moved-aside | |||||
| **/ios/**/*.pbxuser | |||||
| **/ios/**/*.perspectivev3 | |||||
| **/ios/**/*sync/ | |||||
| **/ios/**/.sconsign.dblite | |||||
| **/ios/**/.tags* | |||||
| **/ios/**/.vagrant/ | |||||
| **/ios/**/DerivedData/ | |||||
| **/ios/**/Icon? | |||||
| **/ios/**/Icon?.* | |||||
| **/ios/**/Info.plist | |||||
| **/ios/**/Pods/ | |||||
| **/ios/**/.symlinks/ | |||||
| **/ios/**/Flutter/Flutter.framework | |||||
| **/ios/**/Flutter/Flutter.podspec | |||||
| **/ios/**/Flutter/GeneratedPluginRegistrant.* | |||||
| **/ios/**/generated/ | |||||
| **/ios/**/ephemeral/ | |||||
| **/ios/Flutter/Flutter.podspec | |||||
| **/ios/Flutter/Flutter.framework | |||||
| # Coverage | |||||
| coverage/ | |||||
| # Exceptions to above rules. | |||||
| !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages |
| # This file tracks properties of this Flutter project. | |||||
| # Used by Flutter tool to assess capabilities and perform upgrades etc. | |||||
| # | |||||
| # This file should be version controlled and should not be manually edited. | |||||
| version: | |||||
| revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" | |||||
| channel: "stable" | |||||
| project_type: app | |||||
| # Tracks metadata for the flutter migrate command | |||||
| migration: | |||||
| platforms: | |||||
| - platform: root | |||||
| create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 | |||||
| base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 | |||||
| - platform: android | |||||
| create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 | |||||
| base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 | |||||
| # User provided section | |||||
| # List of Local paths (relative to this file) that should be | |||||
| # ignored by the migrate tool. | |||||
| # | |||||
| # Files that are not part of the templates will be ignored by default. | |||||
| unmanaged_files: | |||||
| - 'lib/main.dart' | |||||
| - 'ios/Runner.xcodeproj/project.pbxproj' |
| # Social Content Creator - Flutter 3.38 | |||||
| Application Flutter cross-platform (Android/iOS) pour créer et partager du contenu optimisé pour réseaux sociaux avec assistance IA. | |||||
| ## 🚀 Prérequis | |||||
| - **Flutter** : 3.38.0+ | |||||
| - **Dart** : 3.10.0+ | |||||
| - **Android SDK** : minSdk 26, targetSdk 35 | |||||
| - **Xcode** : 15.0+ (pour iOS) | |||||
| - **Kotlin** : 2.0.0+ | |||||
| - **AGP** : 8.5.1+ | |||||
| ## ⚙️ Installation | |||||
| ### 1. Cloner et installer les dépendances | |||||
| ```bash | |||||
| unzip social_content_creator.zip | |||||
| cd social_content_creator | |||||
| flutter pub get | |||||
| ``` | |||||
| ### 2. Vérifier l'installation | |||||
| ```bash | |||||
| flutter doctor | |||||
| flutter pub get | |||||
| ``` | |||||
| ### 3. Lancer en développement | |||||
| ```bash | |||||
| flutter run | |||||
| ``` | |||||
| ### 4. Build pour production | |||||
| **Android APK :** | |||||
| ```bash | |||||
| flutter build apk --release | |||||
| ``` | |||||
| **Android App Bundle (Play Store) :** | |||||
| ```bash | |||||
| flutter build appbundle --release | |||||
| ``` | |||||
| **iOS :** | |||||
| ```bash | |||||
| flutter build ios --release | |||||
| ``` | |||||
| ## 📱 Fonctionnalités | |||||
| ✅ Configuration multi-profils (métier, ton, style) | |||||
| ✅ Import/capture d'images et vidéos | |||||
| ✅ Amélioration IA avec 3 versions | |||||
| ✅ Génération de texte personnalisé | |||||
| ✅ Aperçu de post (Instagram/Facebook) | |||||
| ✅ Partage sur réseaux sociaux (Instagram, Facebook, X, LinkedIn, TikTok) | |||||
| ✅ Support Material Design 3 | |||||
| ✅ Navigation nommée avec routes typées | |||||
| ✅ Gestion d'état avec Provider/Riverpod | |||||
| ## 🏗️ Architecture | |||||
| ``` | |||||
| lib/ | |||||
| ├── core/ # Thème, constantes, utilitaires | |||||
| ├── data/ # Modèles, services, repositories | |||||
| ├── presentation/ # UI (écrans, widgets) | |||||
| ├── routes/ # Navigation | |||||
| └── main.dart # Entry point | |||||
| ``` | |||||
| ## 🛠️ Configuration API | |||||
| Éditer `lib/data/services/api_service.dart` et ajouter vos endpoints : | |||||
| ```dart | |||||
| static const String baseUrl = 'https://votre-api.com'; | |||||
| ``` | |||||
| ## 🎨 Design System | |||||
| - **Couleur primaire** : Indigo (#6366F1) | |||||
| - **Couleur accent** : Amber (#F59E0B) | |||||
| - **Police** : Inter (Google Fonts) | |||||
| - **Border radius** : 12-16px | |||||
| - **Spacing** : 8, 12, 16, 20, 24, 32px | |||||
| ## 📦 Packages principaux | |||||
| - `image_picker` : Sélection/capture de médias | |||||
| - `dio` : Requêtes HTTP | |||||
| - `share_plus` : Partage social | |||||
| - `google_fonts` : Typographie | |||||
| - `provider` : État de l'app | |||||
| - `path_provider` : Accès fichiers | |||||
| ## ✅ Versions modernes | |||||
| - Flutter 3.38 (novembre 2025) | |||||
| - Dart 3.10 | |||||
| - Android Gradle Plugin 8.5.1 | |||||
| - Kotlin 2.0.0 | |||||
| - Material 3 | |||||
| ## 📚 Documentation | |||||
| Voir `Social_Content_Creator_Documentation.pdf` pour la documentation complète. | |||||
| --- | |||||
| **Version 1.0.0 - Novembre 2025** |
| # This file configures the analyzer, which statically analyzes Dart code to | |||||
| # check for errors, warnings, and lints. | |||||
| include: package:flutter_lints/flutter.yaml | |||||
| linter: | |||||
| rules: | |||||
| # Error Rules | |||||
| - avoid_empty_else | |||||
| - avoid_print | |||||
| - avoid_relative_lib_imports | |||||
| - avoid_returning_null_for_future | |||||
| - cancel_subscriptions | |||||
| - close_sinks | |||||
| - comment_references | |||||
| - control_flow_in_finally | |||||
| - empty_statements | |||||
| - hash_and_equals | |||||
| - invariant_booleans | |||||
| - iterable_contains_unrelated_type | |||||
| - list_remove_unrelated_type | |||||
| - literal_only_boolean_expressions | |||||
| - no_adjacent_strings_in_list | |||||
| - no_duplicate_case_values | |||||
| - prefer_void_to_null | |||||
| - throw_in_finally | |||||
| - unnecessary_statements | |||||
| - unrelated_type_equality_checks | |||||
| # Style Rules | |||||
| - always_declare_return_types | |||||
| - always_put_control_body_on_new_line | |||||
| - always_put_required_named_parameters_first | |||||
| - annotate_overrides | |||||
| - avoid_bool_literals_in_conditional_expressions | |||||
| - avoid_catches_without_on_clauses | |||||
| - avoid_catching_errors | |||||
| - avoid_double_and_int_checks | |||||
| - avoid_field_initializers_in_const_classes | |||||
| - avoid_function_literals_in_foreach_calls | |||||
| - avoid_init_to_null | |||||
| - avoid_null_checks_in_equality_operators | |||||
| - avoid_positional_boolean_parameters | |||||
| - avoid_private_typedef_functions | |||||
| - avoid_renaming_method_parameters | |||||
| - avoid_returning_null | |||||
| - avoid_returning_null_for_async | |||||
| - avoid_returning_this | |||||
| - avoid_setters_without_getters | |||||
| - avoid_shadowing_type_parameters | |||||
| - avoid_slow_async_io | |||||
| - avoid_types_as_parameter_names | |||||
| - avoid_types_on_closure_parameters | |||||
| - avoid_unnecessary_containers | |||||
| - avoid_unnecessary_setstate | |||||
| - await_only_futures | |||||
| - camel_case_extensions | |||||
| - camel_case_types | |||||
| - cascade_invocations | |||||
| - cast_nullable_to_non_nullable | |||||
| - constant_identifier_names | |||||
| - curly_braces_in_flow_control_structures | |||||
| - directives_ordering | |||||
| - empty_catches | |||||
| - empty_constructor_bodies | |||||
| - eol_at_end_of_file | |||||
| - file_names | |||||
| - implementation_imports | |||||
| - leading_newlines_in_multiline_strings | |||||
| - library_names | |||||
| - library_prefixes | |||||
| - library_private_types_in_public_api | |||||
| - lines_longer_than_80_chars | |||||
| - no_leading_underscores_for_library_prefixes | |||||
| - null_closures | |||||
| - omit_local_variable_types | |||||
| - one_member_abstracts | |||||
| - only_throw_errors | |||||
| - overridden_fields | |||||
| - package_api_docs | |||||
| - package_names | |||||
| - package_prefixed_library_names | |||||
| - parameter_assignments | |||||
| - prefer_adjacent_string_concatenation | |||||
| - prefer_asserts_in_initializer_lists | |||||
| - prefer_asserts_with_message | |||||
| - prefer_collection_literals | |||||
| - prefer_conditional_assignment | |||||
| - prefer_const_constructors | |||||
| - prefer_const_constructors_in_immutables | |||||
| - prefer_const_declarations | |||||
| - prefer_const_literals_to_create_immutables | |||||
| - prefer_constructors_over_static_methods | |||||
| - prefer_contains | |||||
| - prefer_equal_for_default_values | |||||
| - prefer_expression_function_bodies | |||||
| - prefer_final_fields | |||||
| - prefer_final_in_for_each | |||||
| - prefer_final_locals | |||||
| - prefer_for_elements_to_map_fromIterable | |||||
| - prefer_foreach | |||||
| - prefer_function_declarations_over_variables | |||||
| - prefer_generic_function_type_aliases | |||||
| - prefer_if_elements_to_conditional_expressions | |||||
| - prefer_if_null_to_conditional_expressions | |||||
| - prefer_if_on_single_line_statements | |||||
| - prefer_inline_and_then | |||||
| - prefer_inlined_adds | |||||
| - prefer_int_literals | |||||
| - prefer_interpolation_to_compose_strings | |||||
| - prefer_is_empty | |||||
| - prefer_is_not_empty | |||||
| - prefer_is_not_operator | |||||
| - prefer_is_operator | |||||
| - prefer_iterable_whereType | |||||
| - prefer_null_aware_operators | |||||
| - prefer_null_coalescing_operator | |||||
| - prefer_null_coalescing_operators | |||||
| - prefer_on_platform_annotation | |||||
| - prefer_relative_import_paths | |||||
| - prefer_relative_imports | |||||
| - prefer_set_literal | |||||
| - prefer_single_quotes | |||||
| - provide_deprecation_message | |||||
| - recursive_getters | |||||
| - sized_box_for_spacer | |||||
| - sized_box_shrink_expand | |||||
| - slash_for_doc_comments | |||||
| - sort_child_properties_last | |||||
| - sort_constructors_first | |||||
| - sort_pub_dependencies | |||||
| - sort_unnamed_constructors_first | |||||
| - tighten_type_of_initializing_formals | |||||
| - type_annotate_public_apis | |||||
| - type_init_formals | |||||
| - unawaited_futures | |||||
| - unnecessary_await_in_return | |||||
| - unnecessary_brace_in_string_interps | |||||
| - unnecessary_const | |||||
| - unnecessary_constructor_name | |||||
| - unnecessary_getters_setters | |||||
| - unnecessary_lambdas | |||||
| - unnecessary_null_aware_assignments | |||||
| - unnecessary_null_in_if_null_operators | |||||
| - unnecessary_null_checks | |||||
| - unnecessary_null_in_return_type_checks | |||||
| - unnecessary_nullable_for_final_variable_declarations | |||||
| - unnecessary_parenthesis | |||||
| - unnecessary_raw_strings | |||||
| - unnecessary_string_escapes | |||||
| - unnecessary_string_interpolations | |||||
| - unnecessary_to_list_in_spreads | |||||
| - unsafe_html | |||||
| - use_build_context_synchronously | |||||
| - use_full_hex_values_for_flutter_colors | |||||
| - use_function_type_syntax_for_parameters | |||||
| - use_if_null_to_convert_nulls | |||||
| - use_is_even_rather_than_modulo | |||||
| - use_key_in_widget_constructors | |||||
| - use_late_for_private_fields_and_variables | |||||
| - use_named_constants | |||||
| - use_raw_strings | |||||
| - use_rethrow_when_possible | |||||
| - use_setters_to_change_properties | |||||
| - use_string_buffers | |||||
| - use_test_throws_matchers | |||||
| - use_to_close_resource | |||||
| - use_trailing_commas | |||||
| - void_checks | |||||
| - wildcard_import_warning |
| gradle-wrapper.jar | |||||
| /.gradle | |||||
| /captures/ | |||||
| /gradlew | |||||
| /gradlew.bat | |||||
| /local.properties | |||||
| GeneratedPluginRegistrant.java | |||||
| .cxx/ | |||||
| # Remember to never publicly share your keystore. | |||||
| # See https://flutter.dev/to/reference-keystore | |||||
| key.properties | |||||
| **/*.keystore | |||||
| **/*.jks |
| plugins { | |||||
| id("com.android.application") | |||||
| id("kotlin-android") | |||||
| id("dev.flutter.flutter-gradle-plugin") | |||||
| } | |||||
| android { | |||||
| namespace = "com.example.social_content_creator" | |||||
| compileSdk = 36 | |||||
| defaultConfig { | |||||
| applicationId = "com.example.social_content_creator" | |||||
| minSdk = flutter.minSdkVersion | |||||
| targetSdk = 35 | |||||
| versionCode = 1 | |||||
| versionName = "1.0" | |||||
| } | |||||
| buildTypes { | |||||
| release { | |||||
| isMinifyEnabled = false | |||||
| isShrinkResources = false | |||||
| } | |||||
| } | |||||
| /* flavorDimensions.add("version") | |||||
| productFlavors { | |||||
| create("free") { | |||||
| dimension = "version" | |||||
| } | |||||
| }*/ | |||||
| compileOptions { | |||||
| sourceCompatibility = JavaVersion.VERSION_11 | |||||
| targetCompatibility = JavaVersion.VERSION_11 | |||||
| } | |||||
| kotlinOptions { | |||||
| jvmTarget = JavaVersion.VERSION_11.toString() | |||||
| } | |||||
| } | |||||
| dependencies { | |||||
| implementation("androidx.core:core-splashscreen:1.0.1") | |||||
| implementation("androidx.appcompat:appcompat:1.7.0") | |||||
| implementation("androidx.activity:activity-ktx:1.9.1") | |||||
| implementation("com.google.android.material:material:1.12.0") | |||||
| testImplementation("junit:junit:4.13.2") | |||||
| androidTestImplementation("androidx.test:runner:1.6.2") | |||||
| androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") | |||||
| } |
| # Flutter wrapper | |||||
| -keep class io.flutter.app.** { *; } | |||||
| -keep class io.flutter.plugin.** { *; } | |||||
| -keep class io.flutter.util.** { *; } | |||||
| -keep class io.flutter.view.** { *; } | |||||
| -keep class io.flutter.** { *; } | |||||
| -keep class io.flutter.plugins.** { *; } | |||||
| # Firebase (if used) | |||||
| -keep class com.google.firebase.** { *; } | |||||
| # Preserve line numbers for debugging crashes | |||||
| -keepattributes SourceFile,LineNumberTable | |||||
| -renamesourcefileattribute SourceFile |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | |||||
| <!-- The INTERNET permission is required for development. Specifically, | |||||
| the Flutter tool needs it to communicate with the running application | |||||
| to allow setting breakpoints, to provide hot reload, etc. | |||||
| --> | |||||
| <uses-permission android:name="android.permission.INTERNET"/> | |||||
| </manifest> |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | |||||
| package="com.example.social_content_creator"> | |||||
| <!-- Permissions for camera, gallery, and file access --> | |||||
| <uses-permission android:name="android.permission.INTERNET" /> | |||||
| <uses-permission android:name="android.permission.CAMERA" /> | |||||
| <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | |||||
| <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | |||||
| <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> | |||||
| <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> | |||||
| <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | |||||
| <application | |||||
| android:label="Social Content Creator" | |||||
| android:name="${applicationName}" | |||||
| android:icon="@mipmap/ic_launcher" | |||||
| android:usesCleartextTraffic="false"> | |||||
| <activity | |||||
| android:name=".MainActivity" | |||||
| android:exported="true" | |||||
| android:launchMode="singleTop" | |||||
| android:theme="@style/LaunchTheme" | |||||
| android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" | |||||
| android:hardwareAccelerated="true" | |||||
| android:windowSoftInputMode="adjustResize"> | |||||
| <meta-data | |||||
| android:name="io.flutter.embedding.android.NormalTheme" | |||||
| android:resource="@style/NormalTheme" /> | |||||
| <intent-filter> | |||||
| <action android:name="android.intent.action.MAIN" /> | |||||
| <category android:name="android.intent.category.LAUNCHER" /> | |||||
| </intent-filter> | |||||
| </activity> | |||||
| <meta-data | |||||
| android:name="flutterEmbedding" | |||||
| android:value="2" /> | |||||
| <!-- FileProvider for sharing --> | |||||
| <provider | |||||
| android:name="androidx.core.content.FileProvider" | |||||
| android:authorities="${applicationId}.fileprovider" | |||||
| android:exported="false" | |||||
| android:grantUriPermissions="true"> | |||||
| <meta-data | |||||
| android:name="android.support.FILE_PROVIDER_PATHS" | |||||
| android:resource="@xml/file_paths" /> | |||||
| </provider> | |||||
| </application> | |||||
| </manifest> |
| package com.example.social_content_creator | |||||
| import io.flutter.embedding.android.FlutterActivity | |||||
| import io.flutter.embedding.engine.FlutterEngine | |||||
| import io.flutter.plugins.GeneratedPluginRegistrant | |||||
| class MainActivity: FlutterActivity() { | |||||
| override fun configureFlutterEngine(flutterEngine: FlutterEngine) { | |||||
| GeneratedPluginRegistrant.registerWith(flutterEngine) | |||||
| super.configureFlutterEngine(flutterEngine) | |||||
| } | |||||
| } |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <!-- Modify this file to customize your launch splash screen --> | |||||
| <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> | |||||
| <item android:drawable="?android:colorBackground" /> | |||||
| <!-- You can insert your own image assets here --> | |||||
| <!-- <item> | |||||
| <bitmap | |||||
| android:gravity="center" | |||||
| android:src="@mipmap/launch_image" /> | |||||
| </item> --> | |||||
| </layer-list> |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <!-- Modify this file to customize your launch splash screen --> | |||||
| <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> | |||||
| <item android:drawable="@android:color/white" /> | |||||
| <!-- You can insert your own image assets here --> | |||||
| <!-- <item> | |||||
| <bitmap | |||||
| android:gravity="center" | |||||
| android:src="@mipmap/launch_image" /> | |||||
| </item> --> | |||||
| </layer-list> |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <resources> | |||||
| <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on --> | |||||
| <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> | |||||
| <!-- Show a splash screen on the activity. Automatically removed when | |||||
| the Flutter engine draws its first frame --> | |||||
| <item name="android:windowBackground">@drawable/launch_background</item> | |||||
| </style> | |||||
| <!-- Theme applied to the Android Window as soon as the process has started. | |||||
| This theme determines the color of the Android Window while your | |||||
| Flutter UI initializes, as well as behind your Flutter UI while its | |||||
| running. | |||||
| This Theme is only used starting with V2 of Flutter's Android embedding. --> | |||||
| <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> | |||||
| <item name="android:windowBackground">?android:colorBackground</item> | |||||
| </style> | |||||
| </resources> |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <resources> | |||||
| <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off --> | |||||
| <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar"> | |||||
| <!-- Show a splash screen on the activity. Automatically removed when | |||||
| the Flutter engine draws its first frame --> | |||||
| <item name="android:windowBackground">@drawable/launch_background</item> | |||||
| </style> | |||||
| <!-- Theme applied to the Android Window as soon as the process has started. | |||||
| This theme determines the color of the Android Window while your | |||||
| Flutter UI initializes, as well as behind your Flutter UI while its | |||||
| running. | |||||
| This Theme is only used starting with V2 of Flutter's Android embedding. --> | |||||
| <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar"> | |||||
| <item name="android:windowBackground">?android:colorBackground</item> | |||||
| </style> | |||||
| </resources> |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <paths xmlns:android="http://schemas.android.com/apk/res/android"> | |||||
| <external-path name="external_files" path="."/> | |||||
| <cache-path name="cache" path="." /> | |||||
| <files-path name="files" path="." /> | |||||
| </paths> |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | |||||
| <!-- The INTERNET permission is required for development. Specifically, | |||||
| the Flutter tool needs it to communicate with the running application | |||||
| to allow setting breakpoints, to provide hot reload, etc. | |||||
| --> | |||||
| <uses-permission android:name="android.permission.INTERNET"/> | |||||
| </manifest> |
| allprojects { | |||||
| repositories { | |||||
| google() | |||||
| mavenCentral() | |||||
| } | |||||
| } | |||||
| rootProject.layout.buildDirectory.set(file("../build")) | |||||
| subprojects { | |||||
| project.layout.buildDirectory.set(file("${rootProject.layout.buildDirectory.get()}/${project.name}")) | |||||
| } | |||||
| subprojects { | |||||
| project.evaluationDependsOn(":app") | |||||
| } | |||||
| tasks.register<Delete>("clean") { | |||||
| delete(rootProject.layout.buildDirectory) | |||||
| } |
| properties | |||||
| org.gradle.java.home=C:\\Program Files\\Android\\Android Studio\\jbr | |||||
| org.gradle.jvmargs=-Xmx4096m | |||||
| org.gradle.parallel=true | |||||
| org.gradle.workers.max=4 | |||||
| android.useAndroidX=true | |||||
| android.enableJetifier=true |
| distributionBase=GRADLE_USER_HOME | |||||
| distributionPath=wrapper/dists | |||||
| zipStoreBase=GRADLE_USER_HOME | |||||
| zipStorePath=wrapper/dists | |||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip |
| pluginManagement { | |||||
| val flutterSdkPath = run { | |||||
| val properties = java.util.Properties() | |||||
| val localPropertiesFile = java.io.File(rootProject.projectDir, "local.properties") | |||||
| if (localPropertiesFile.exists()) { | |||||
| java.io.FileInputStream(localPropertiesFile).use { stream -> | |||||
| properties.load(stream) | |||||
| } | |||||
| } | |||||
| val flutterSdk = properties.getProperty("flutter.sdk") | |||||
| check(flutterSdk != null) { | |||||
| """ | |||||
| Flutter SDK not found. | |||||
| Define location with flutter.sdk in the local.properties file. | |||||
| """.trimIndent() | |||||
| } | |||||
| flutterSdk | |||||
| } | |||||
| includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") | |||||
| repositories { | |||||
| google() | |||||
| mavenCentral() | |||||
| gradlePluginPortal() | |||||
| } | |||||
| } | |||||
| plugins { | |||||
| id("dev.flutter.flutter-plugin-loader") version "1.0.0" | |||||
| id("com.android.application") version "8.6.0" apply false | |||||
| id("org.jetbrains.kotlin.android") version "2.1.0" apply false | |||||
| } | |||||
| include(":app") |
| description: This file stores settings for Dart & Flutter DevTools. | |||||
| documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states | |||||
| extensions: |
| org.gradle.jvmargs=-Xmx4096m | |||||
| android.useAndroidX=true | |||||
| android.enableJetifier=true |
| // This is a generated file; do not edit or check into version control. | |||||
| FLUTTER_ROOT=C:\src\flutter | |||||
| FLUTTER_APPLICATION_PATH=C:\Users\yann\AndroidStudioProjects\social_content_creator | |||||
| COCOAPODS_PARALLEL_CODE_SIGN=true | |||||
| FLUTTER_TARGET=lib\main.dart | |||||
| FLUTTER_BUILD_DIR=build | |||||
| FLUTTER_BUILD_NAME=1.0.0 | |||||
| FLUTTER_BUILD_NUMBER=1 | |||||
| EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 | |||||
| EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 | |||||
| DART_OBFUSCATION=false | |||||
| TRACK_WIDGET_CREATION=true | |||||
| TREE_SHAKE_ICONS=false | |||||
| PACKAGE_CONFIG=.dart_tool/package_config.json |
| #!/bin/sh | |||||
| # This is a generated file; do not edit or check into version control. | |||||
| export "FLUTTER_ROOT=C:\src\flutter" | |||||
| export "FLUTTER_APPLICATION_PATH=C:\Users\yann\AndroidStudioProjects\social_content_creator" | |||||
| export "COCOAPODS_PARALLEL_CODE_SIGN=true" | |||||
| export "FLUTTER_TARGET=lib\main.dart" | |||||
| export "FLUTTER_BUILD_DIR=build" | |||||
| export "FLUTTER_BUILD_NAME=1.0.0" | |||||
| export "FLUTTER_BUILD_NUMBER=1" | |||||
| export "DART_OBFUSCATION=false" | |||||
| export "TRACK_WIDGET_CREATION=true" | |||||
| export "TREE_SHAKE_ICONS=false" | |||||
| export "PACKAGE_CONFIG=.dart_tool/package_config.json" |
| # Podfile for Flutter 3.38 | |||||
| # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | |||||
| ENV['COCOAPODS_DISABLE_STATS'] = 'true' | |||||
| project 'Runner', { | |||||
| 'Debug' => :debug, | |||||
| 'Profile' => :release, | |||||
| 'Release' => :release, | |||||
| } | |||||
| def flutter_root | |||||
| generated_xcode_build_settings_path = File.expand_path(File.join( | |||||
| File.dirname(__FILE__), 'Flutter', 'Generated.xcconfig'), __FILE__) | |||||
| unless File.exist?(generated_xcode_build_settings_path) | |||||
| raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" | |||||
| end | |||||
| File.foreach(generated_xcode_build_settings_path) do |line| | |||||
| matches = line.match(/FLUTTER_ROOT\=(.*)/) | |||||
| return matches[1].strip if matches | |||||
| end | |||||
| raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" | |||||
| end | |||||
| require File.expand_path(File.join(packages_path, 'flutter_tools', 'bin', 'podhelper'), flutter_root) | |||||
| flutter_ios_podfile_setup | |||||
| target 'Runner' do | |||||
| use_frameworks! | |||||
| use_modular_headers! | |||||
| flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) | |||||
| end | |||||
| post_install do |installer| | |||||
| installer.pods_project.targets.each do |target| | |||||
| flutter_additional_ios_build_settings(target) | |||||
| target.build_configurations.each do |config| | |||||
| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ | |||||
| '$(inherited)', | |||||
| 'PERMISSION_CAMERA=1', | |||||
| 'PERMISSION_PHOTOS=1', | |||||
| ] | |||||
| end | |||||
| end | |||||
| end |
| // | |||||
| // Generated file. Do not edit. | |||||
| // | |||||
| // clang-format off | |||||
| #ifndef GeneratedPluginRegistrant_h | |||||
| #define GeneratedPluginRegistrant_h | |||||
| #import <Flutter/Flutter.h> | |||||
| NS_ASSUME_NONNULL_BEGIN | |||||
| @interface GeneratedPluginRegistrant : NSObject | |||||
| + (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry; | |||||
| @end | |||||
| NS_ASSUME_NONNULL_END | |||||
| #endif /* GeneratedPluginRegistrant_h */ |
| // | |||||
| // Generated file. Do not edit. | |||||
| // | |||||
| // clang-format off | |||||
| #import "GeneratedPluginRegistrant.h" | |||||
| #if __has_include(<flutter_facebook_auth/FlutterFacebookAuthPlugin.h>) | |||||
| #import <flutter_facebook_auth/FlutterFacebookAuthPlugin.h> | |||||
| #else | |||||
| @import flutter_facebook_auth; | |||||
| #endif | |||||
| #if __has_include(<flutter_secure_storage/FlutterSecureStoragePlugin.h>) | |||||
| #import <flutter_secure_storage/FlutterSecureStoragePlugin.h> | |||||
| #else | |||||
| @import flutter_secure_storage; | |||||
| #endif | |||||
| #if __has_include(<image_picker_ios/FLTImagePickerPlugin.h>) | |||||
| #import <image_picker_ios/FLTImagePickerPlugin.h> | |||||
| #else | |||||
| @import image_picker_ios; | |||||
| #endif | |||||
| #if __has_include(<path_provider_foundation/PathProviderPlugin.h>) | |||||
| #import <path_provider_foundation/PathProviderPlugin.h> | |||||
| #else | |||||
| @import path_provider_foundation; | |||||
| #endif | |||||
| #if __has_include(<share_plus/FPPSharePlusPlugin.h>) | |||||
| #import <share_plus/FPPSharePlusPlugin.h> | |||||
| #else | |||||
| @import share_plus; | |||||
| #endif | |||||
| #if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>) | |||||
| #import <shared_preferences_foundation/SharedPreferencesPlugin.h> | |||||
| #else | |||||
| @import shared_preferences_foundation; | |||||
| #endif | |||||
| #if __has_include(<sqflite_darwin/SqflitePlugin.h>) | |||||
| #import <sqflite_darwin/SqflitePlugin.h> | |||||
| #else | |||||
| @import sqflite_darwin; | |||||
| #endif | |||||
| @implementation GeneratedPluginRegistrant | |||||
| + (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry { | |||||
| [FlutterFacebookAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterFacebookAuthPlugin"]]; | |||||
| [FlutterSecureStoragePlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterSecureStoragePlugin"]]; | |||||
| [FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]]; | |||||
| [PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]]; | |||||
| [FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]]; | |||||
| [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; | |||||
| [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; | |||||
| } | |||||
| @end |
| import 'package:flutter/material.dart'; | |||||
| import 'package:google_fonts/google_fonts.dart'; | |||||
| import 'colors.dart'; | |||||
| class AppTheme { | |||||
| static ThemeData get lightTheme { | |||||
| return ThemeData( | |||||
| useMaterial3: true, | |||||
| colorScheme: ColorScheme.fromSeed( | |||||
| seedColor: AppColors.primary, | |||||
| brightness: Brightness.light, | |||||
| ), | |||||
| scaffoldBackgroundColor: AppColors.background, | |||||
| textTheme: GoogleFonts.interTextTheme(ThemeData.light().textTheme), | |||||
| appBarTheme: AppBarTheme( | |||||
| backgroundColor: Colors.white, | |||||
| elevation: 0, | |||||
| centerTitle: true, | |||||
| scrolledUnderElevation: 0, | |||||
| titleTextStyle: GoogleFonts.inter( | |||||
| fontSize: 20, | |||||
| fontWeight: FontWeight.w600, | |||||
| color: AppColors.textPrimary, | |||||
| ), | |||||
| iconTheme: const IconThemeData(color: AppColors.textPrimary), | |||||
| ), | |||||
| elevatedButtonTheme: ElevatedButtonThemeData( | |||||
| style: ElevatedButton.styleFrom( | |||||
| backgroundColor: AppColors.primary, | |||||
| foregroundColor: Colors.white, | |||||
| padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), | |||||
| shape: RoundedRectangleBorder( | |||||
| borderRadius: BorderRadius.circular(12), | |||||
| ), | |||||
| elevation: 0, | |||||
| textStyle: GoogleFonts.inter( | |||||
| fontSize: 16, | |||||
| fontWeight: FontWeight.w600, | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| outlinedButtonTheme: OutlinedButtonThemeData( | |||||
| style: OutlinedButton.styleFrom( | |||||
| padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), | |||||
| shape: RoundedRectangleBorder( | |||||
| borderRadius: BorderRadius.circular(12), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| inputDecorationTheme: InputDecorationTheme( | |||||
| filled: true, | |||||
| fillColor: Colors.grey[50], | |||||
| border: OutlineInputBorder( | |||||
| borderRadius: BorderRadius.circular(12), | |||||
| borderSide: BorderSide(color: Colors.grey[300]!), | |||||
| ), | |||||
| enabledBorder: OutlineInputBorder( | |||||
| borderRadius: BorderRadius.circular(12), | |||||
| borderSide: BorderSide(color: Colors.grey[300]!), | |||||
| ), | |||||
| focusedBorder: OutlineInputBorder( | |||||
| borderRadius: BorderRadius.circular(12), | |||||
| borderSide: const BorderSide(color: AppColors.primary, width: 2), | |||||
| ), | |||||
| contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), | |||||
| ), | |||||
| cardTheme: CardThemeData( | |||||
| elevation: 0, | |||||
| shape: RoundedRectangleBorder( | |||||
| borderRadius: BorderRadius.circular(16), | |||||
| side: BorderSide(color: Colors.grey[200]!), | |||||
| ), | |||||
| color: Colors.white, | |||||
| ), | |||||
| chipTheme: ChipThemeData( | |||||
| padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), | |||||
| labelPadding: const EdgeInsets.symmetric(horizontal: 8), | |||||
| shape: RoundedRectangleBorder( | |||||
| borderRadius: BorderRadius.circular(24), | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| static ThemeData get darkTheme { | |||||
| return ThemeData( | |||||
| useMaterial3: true, | |||||
| colorScheme: ColorScheme.fromSeed( | |||||
| seedColor: AppColors.primary, | |||||
| brightness: Brightness.dark, | |||||
| ), | |||||
| scaffoldBackgroundColor: const Color(0xFF121212), | |||||
| ); | |||||
| } | |||||
| } |
| import 'package:flutter/material.dart'; | |||||
| /// Palettes de couleurs pour l'application Social Content Creator | |||||
| abstract final class AppColors { | |||||
| // Primary Colors | |||||
| static const Color primary = Color(0xFF6366F1); // Indigo | |||||
| static const Color primaryDark = Color(0xFF4F46E5); | |||||
| static const Color primaryLight = Color(0xFF818CF8); | |||||
| // Accent Colors | |||||
| static const Color accent = Color(0xFFF59E0B); // Amber | |||||
| static const Color accentLight = Color(0xFFFBBF24); | |||||
| // Background Colors | |||||
| static const Color background = Color(0xFFF9FAFB); | |||||
| static const Color cardBackground = Colors.white; | |||||
| static const Color surface = Color(0xFFF3F4F6); | |||||
| // Text Colors | |||||
| static const Color textPrimary = Color(0xFF111827); | |||||
| static const Color textSecondary = Color(0xFF6B7280); | |||||
| static const Color textTertiary = Color(0xFF9CA3AF); | |||||
| // Tone Colors | |||||
| static const Color toneFun = Color(0xFFEC4899); // Pink | |||||
| static const Color toneSerious = Color(0xFF3B82F6); // Blue | |||||
| static const Color toneNormal = Color(0xFF10B981); // Green | |||||
| static const Color toneOleOle = Color(0xFFF59E0B); // Orange | |||||
| // Status Colors | |||||
| static const Color success = Color(0xFF10B981); | |||||
| static const Color error = Color(0xFFEF4444); | |||||
| static const Color warning = Color(0xFFF59E0B); | |||||
| static const Color info = Color(0xFF3B82F6); | |||||
| // Social Media Colors | |||||
| static const Color instagram = Color(0xFFE4405F); | |||||
| static const Color facebook = Color(0xFF1877F2); | |||||
| static const Color twitter = Color(0xFF1DA1F2); | |||||
| static const Color linkedin = Color(0xFF0A66C2); | |||||
| static const Color tiktok = Color(0xFF000000); | |||||
| } |
| import 'dart:io'; | |||||
| /// Enum pour le type de média | |||||
| enum MediaType { | |||||
| image, | |||||
| video; | |||||
| String get label => name.toUpperCase(); | |||||
| } | |||||
| /// Enum pour les plateformes sociales | |||||
| enum SocialPlatform { | |||||
| instagram('Instagram', '📸', 0xFFE4405F), | |||||
| facebook('Facebook', '👍', 0xFF1877F2), | |||||
| tiktok('TikTok', '🎵', 0xFF000000), | |||||
| twitter('X', '𝕏', 0xFF1DA1F2), | |||||
| linkedin('LinkedIn', '💼', 0xFF0A66C2); | |||||
| final String displayName; | |||||
| final String emoji; | |||||
| final int colorValue; | |||||
| const SocialPlatform(this.displayName, this.emoji, this.colorValue); | |||||
| } | |||||
| /// Modèle pour un post de contenu (Dart 3.10) | |||||
| final class ContentPost { | |||||
| /// Modèle pour un post de contenu (Dart 3.10)final class ContentPost { | |||||
| final File? originalMedia; | |||||
| final File? enhancedMedia; | |||||
| final MediaType mediaType; | |||||
| final String? generatedText; | |||||
| final bool includeEmojis; | |||||
| final bool includeCommercialInfo; | |||||
| final Set<SocialPlatform> selectedPlatforms; | |||||
| final DateTime createdAt; | |||||
| // CORRECTION 1 : Le mot-clé 'const' est retiré du constructeur. | |||||
| ContentPost({ | |||||
| this.originalMedia, | |||||
| this.enhancedMedia, | |||||
| required this.mediaType, | |||||
| this.generatedText, | |||||
| this.includeEmojis = true, | |||||
| this.includeCommercialInfo = false, | |||||
| this.selectedPlatforms = const {}, | |||||
| DateTime? createdAt, | |||||
| }) : createdAt = createdAt ?? DateTime.now(); // Cette ligne est maintenant valide. | |||||
| /// Utiliser records pour retourner plusieurs valeurs | |||||
| (File?, String?) getMediaAndText() => (enhancedMedia ?? originalMedia, generatedText); | |||||
| ContentPost copyWith({ | |||||
| File? originalMedia, | |||||
| File? enhancedMedia, | |||||
| MediaType? mediaType, | |||||
| String? generatedText, | |||||
| bool? includeEmojis, | |||||
| bool? includeCommercialInfo, | |||||
| Set<SocialPlatform>? selectedPlatforms, | |||||
| // Note : On n'ajoute pas 'createdAt' dans les paramètres ici, | |||||
| // car on veut généralement conserver la date de création originale lors d'une copie. | |||||
| }) { | |||||
| return ContentPost( | |||||
| originalMedia: originalMedia ?? this.originalMedia, | |||||
| enhancedMedia: enhancedMedia ?? this.enhancedMedia, | |||||
| mediaType: mediaType ?? this.mediaType, | |||||
| generatedText: generatedText ?? this.generatedText, | |||||
| includeEmojis: includeEmojis ?? this.includeEmojis, | |||||
| includeCommercialInfo: includeCommercialInfo ?? this.includeCommercialInfo, | |||||
| selectedPlatforms: selectedPlatforms ?? this.selectedPlatforms, | |||||
| // CORRECTION 2 : On passe explicitement la date de création de l'objet actuel ('this'). | |||||
| createdAt: this.createdAt, | |||||
| ); | |||||
| } | |||||
| @override | |||||
| String toString() => | |||||
| 'ContentPost(type: ${mediaType.label}, platforms: ${selectedPlatforms.length})'; | |||||
| } | |||||
| /// Record pour les résultats d'API (Dart 3.10) | |||||
| typedef EnhancementResult = ({ | |||||
| List<File> versions, | |||||
| DateTime processedAt, | |||||
| String? error, | |||||
| }); | |||||
| typedef TextGenerationResult = ({ | |||||
| List<String> proposals, | |||||
| DateTime generatedAt, | |||||
| String? error, | |||||
| }); |
| import 'package:flutter/foundation.dart'; | |||||
| /// Enum pour les tons de message (Dart 3.10 with enhanced members) | |||||
| enum MessageTone { | |||||
| fun('Fun', '😄'), | |||||
| serious('Sérieux', '💼'), | |||||
| normal('Normal', '✨'), | |||||
| oleOle('Olé Olé', '🔥'); | |||||
| final String displayName; | |||||
| final String emoji; | |||||
| const MessageTone(this.displayName, this.emoji); | |||||
| } | |||||
| /// Enum pour le style de texte | |||||
| enum TextStyleEnum { | |||||
| classic('Classique'), | |||||
| modern('Moderne'), | |||||
| playful('Ludique'); | |||||
| final String displayName; | |||||
| const TextStyleEnum(this.displayName); | |||||
| } | |||||
| /// Modèle de profil utilisateur (Dart 3.10) | |||||
| final class UserProfile { | |||||
| final String profession; | |||||
| final MessageTone tone; | |||||
| final TextStyleEnum textStyle; | |||||
| const UserProfile({ | |||||
| required this.profession, | |||||
| required this.tone, | |||||
| required this.textStyle, | |||||
| }); | |||||
| /// Records pour extraction facile des données | |||||
| (String, MessageTone, TextStyleEnum) toRecord() => | |||||
| (profession, tone, textStyle); | |||||
| Map<String, dynamic> toJson() => <String, dynamic>{ | |||||
| 'profession': profession, | |||||
| 'tone': tone.name, | |||||
| 'textStyle': textStyle.name, | |||||
| }; | |||||
| factory UserProfile.fromJson(Map<String, dynamic> json) { | |||||
| return UserProfile( | |||||
| profession: json['profession'] as String? ?? '', | |||||
| tone: MessageTone.values.firstWhere( | |||||
| (e) => e.name == json['tone'], | |||||
| orElse: () => MessageTone.normal, | |||||
| ), | |||||
| textStyle: TextStyleEnum.values.firstWhere( | |||||
| (e) => e.name == json['textStyle'], | |||||
| orElse: () => TextStyleEnum.classic, | |||||
| ), | |||||
| ); | |||||
| } | |||||
| UserProfile copyWith({ | |||||
| String? profession, | |||||
| MessageTone? tone, | |||||
| TextStyleEnum? textStyle, | |||||
| }) { | |||||
| return UserProfile( | |||||
| profession: profession ?? this.profession, | |||||
| tone: tone ?? this.tone, | |||||
| textStyle: textStyle ?? this.textStyle, | |||||
| ); | |||||
| } | |||||
| @override | |||||
| String toString() => | |||||
| 'UserProfile(profession: $profession, tone: ${tone.displayName}, style: ${textStyle.displayName})'; | |||||
| } |
| import 'dart:io'; | |||||
| import 'dart:typed_data'; | |||||
| import 'package:dio/dio.dart'; | |||||
| import 'package:path/path.dart' as path; | |||||
| import '../models/content_post.dart'; | |||||
| /// Service API pour l'amélioration IA et la génération de texte | |||||
| class ApiService { | |||||
| late final Dio _dio; | |||||
| static const String baseUrl = 'https://api.your-ai-provider.com'; | |||||
| static const String aiEnhancementEndpoint = '/api/v1/enhance-image'; | |||||
| static const String textGenerationEndpoint = '/api/v1/generate-text'; | |||||
| ApiService() { | |||||
| _dio = Dio( | |||||
| BaseOptions( | |||||
| baseUrl: baseUrl, | |||||
| connectTimeout: const Duration(seconds: 30), | |||||
| receiveTimeout: const Duration(seconds: 30), | |||||
| sendTimeout: const Duration(seconds: 30), | |||||
| ), | |||||
| ); | |||||
| // Ajouter un interceptor pour les logs (development) | |||||
| _dio.interceptors.add( | |||||
| LogInterceptor( | |||||
| requestBody: true, | |||||
| responseBody: true, | |||||
| error: true, | |||||
| ), | |||||
| ); | |||||
| } | |||||
| /// Améliorer un média via l'API IA | |||||
| Future<EnhancementResult> enhanceMedia( | |||||
| File mediaFile, { | |||||
| required MediaType mediaType, | |||||
| }) async { | |||||
| try { | |||||
| final fileName = path.basename(mediaFile.path); | |||||
| final bytes = await mediaFile.readAsBytes(); | |||||
| final formData = FormData.fromMap({ | |||||
| 'media': MultipartFile.fromBytes(bytes, filename: fileName), | |||||
| 'type': mediaType.name, | |||||
| 'versions': '3', | |||||
| }); | |||||
| final response = await _dio.post<Map<String, dynamic>>( | |||||
| aiEnhancementEndpoint, | |||||
| data: formData, | |||||
| ); | |||||
| if (response.statusCode == 200) { | |||||
| final data = response.data!; | |||||
| final versions = <File>[]; | |||||
| // Parser les URLs retournées et télécharger | |||||
| if (data['urls'] is List) { | |||||
| for (final url in data['urls'] as List) { | |||||
| final file = await _downloadFile(url.toString()); | |||||
| versions.add(file); | |||||
| } | |||||
| } | |||||
| return ( | |||||
| versions: versions, | |||||
| processedAt: DateTime.now(), | |||||
| error: null, | |||||
| ); | |||||
| } else { | |||||
| throw Exception('API error: ${response.statusCode}'); | |||||
| } | |||||
| } on DioException catch (e) { | |||||
| return ( | |||||
| versions: const <File>[], | |||||
| processedAt: DateTime.now(), | |||||
| error: e.message ?? 'Unknown error', | |||||
| ); | |||||
| } catch (e) { | |||||
| return ( | |||||
| versions: const <File>[], | |||||
| processedAt: DateTime.now(), | |||||
| error: e.toString(), | |||||
| ); | |||||
| } | |||||
| } | |||||
| /// Générer du texte basé sur le profil et les préférences | |||||
| Future<TextGenerationResult> generateTextProposals({ | |||||
| required String profession, | |||||
| required String tone, | |||||
| required String style, | |||||
| required bool includeEmojis, | |||||
| required bool includeCommercialInfo, | |||||
| String? mediaDescription, | |||||
| }) async { | |||||
| try { | |||||
| final response = await _dio.post<Map<String, dynamic>>( | |||||
| textGenerationEndpoint, | |||||
| data: { | |||||
| 'profession': profession, | |||||
| 'tone': tone, | |||||
| 'style': style, | |||||
| 'include_emojis': includeEmojis, | |||||
| 'include_commercial_info': includeCommercialInfo, | |||||
| 'media_description': mediaDescription, | |||||
| 'num_proposals': 3, | |||||
| }, | |||||
| ); | |||||
| if (response.statusCode == 200) { | |||||
| final data = response.data!; | |||||
| final proposals = List<String>.from(data['proposals'] as List? ?? []); | |||||
| return ( | |||||
| proposals: proposals, | |||||
| generatedAt: DateTime.now(), | |||||
| error: null, | |||||
| ); | |||||
| } else { | |||||
| throw Exception('API error: ${response.statusCode}'); | |||||
| } | |||||
| } on DioException catch (e) { | |||||
| // Retourner des proposals mock pour le développement | |||||
| return ( | |||||
| proposals: _getMockProposals(includeEmojis), | |||||
| generatedAt: DateTime.now(), | |||||
| error: e.message, | |||||
| ); | |||||
| } catch (e) { | |||||
| return ( | |||||
| proposals: _getMockProposals(includeEmojis), | |||||
| generatedAt: DateTime.now(), | |||||
| error: e.toString(), | |||||
| ); | |||||
| } | |||||
| } | |||||
| /// Télécharger un fichier depuis une URL | |||||
| Future<File> _downloadFile(String urlString) async { | |||||
| try { | |||||
| final response = await _dio.get<Uint8List>( | |||||
| urlString, | |||||
| options: Options(responseType: ResponseType.bytes), | |||||
| ); | |||||
| final dir = Directory.systemTemp; | |||||
| final file = File('${dir.path}/enhanced_${DateTime.now().millisecondsSinceEpoch}.jpg'); | |||||
| await file.writeAsBytes(response.data!); | |||||
| return file; | |||||
| } catch (e) { | |||||
| throw Exception('Failed to download file: $e'); | |||||
| } | |||||
| } | |||||
| /// Propositions mock pour le développement | |||||
| List<String> _getMockProposals(bool withEmojis) { | |||||
| if (withEmojis) { | |||||
| return [ | |||||
| '🌟 Découvrez notre nouvelle collection ! ✨ Qualité premium et innovation réunies. 💼 Rejoignez-nous pour transformer vos projets !', | |||||
| '💡 Excellence et professionnalisme au rendez-vous ! 🎯 Solutions innovantes pour tous. Faites confiance à notre expertise ! 🚀', | |||||
| '🔥 Olé Olé ! 🎉 Créativité et style sans limites. Vos rêves deviennent réalité avec nous ! ✨🌈', | |||||
| ]; | |||||
| } else { | |||||
| return [ | |||||
| 'Découvrez notre nouvelle collection. Qualité premium et innovation réunies. Rejoignez-nous pour transformer vos projets.', | |||||
| 'Excellence et professionnalisme au rendez-vous. Solutions innovantes pour tous. Faites confiance à notre expertise.', | |||||
| 'Créativité et style sans limites. Vos rêves deviennent réalité avec nous.', | |||||
| ]; | |||||
| } | |||||
| } | |||||
| } |
| // lib/services/auth_service.dart | |||||
| import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; | |||||
| // Un modèle simple pour stocker les informations de l'utilisateur connecté | |||||
| class UserProfile { | |||||
| final String name; | |||||
| final String email; | |||||
| final String? pictureUrl; | |||||
| UserProfile({required this.name, required this.email, this.pictureUrl}); | |||||
| } | |||||
| class AuthService { | |||||
| // Propriété pour savoir si l'utilisateur est connecté et garder ses infos | |||||
| UserProfile? _userProfile; | |||||
| UserProfile? get userProfile => _userProfile; | |||||
| bool get isLoggedIn => _userProfile != null; | |||||
| /// Tente de se connecter avec Facebook | |||||
| Future<bool> loginWithFacebook() async { | |||||
| try { | |||||
| print("[AuthService] Tentative de connexion avec Facebook..."); | |||||
| // Lance la popup de connexion Facebook | |||||
| final LoginResult result = await FacebookAuth.instance.login( | |||||
| permissions: ['public_profile', 'email'], // Demande les permissions | |||||
| ); | |||||
| // Vérifie si la connexion a réussi | |||||
| if (result.status == LoginStatus.success) { | |||||
| print("[AuthService] Connexion Facebook réussie."); | |||||
| // Récupère les données de l'utilisateur depuis l'API Graph de Facebook | |||||
| final userData = await FacebookAuth.instance.getUserData( | |||||
| fields: "name,email,picture.width(200)", // Champs demandés | |||||
| ); | |||||
| // Crée notre objet UserProfile | |||||
| _userProfile = UserProfile( | |||||
| name: userData['name'], | |||||
| email: userData['email'], | |||||
| pictureUrl: userData['picture']?['data']?['url'], | |||||
| ); | |||||
| print("[AuthService] Utilisateur connecté : ${_userProfile!.name}"); | |||||
| return true; | |||||
| } else { | |||||
| print("[AuthService] Échec de la connexion : ${result.status}"); | |||||
| print("[AuthService] Message : ${result.message}"); | |||||
| return false; | |||||
| } | |||||
| } catch (e, s) { | |||||
| print("[AuthService] ❌ Erreur lors de la connexion Facebook: $e"); | |||||
| print(s); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| /// Déconnecte l'utilisateur | |||||
| Future<void> logout() async { | |||||
| try { | |||||
| print("[AuthService] Déconnexion..."); | |||||
| await FacebookAuth.instance.logOut(); | |||||
| _userProfile = null; | |||||
| print("[AuthService] Utilisateur déconnecté."); | |||||
| } catch (e) { | |||||
| print("[AuthService] ❌ Erreur lors de la déconnexion: $e"); | |||||
| } | |||||
| } | |||||
| } |
| import 'dart:io'; | |||||
| import 'package:flutter/services.dart'; | |||||
| import 'package:image_picker/image_picker.dart'; | |||||
| import 'package:path_provider/path_provider.dart'; | |||||
| import 'package:path/path.dart' as path; | |||||
| /// Service pour gérer l'accès aux images et vidéos | |||||
| class ImageService { | |||||
| final ImagePicker _picker = ImagePicker(); | |||||
| /// Sélectionner une image depuis la galerie | |||||
| Future<File?> pickImageFromGallery() async { | |||||
| try { | |||||
| final XFile? image = await _picker.pickImage( | |||||
| source: ImageSource.gallery, | |||||
| maxWidth: 1920, | |||||
| maxHeight: 1920, | |||||
| imageQuality: 90, | |||||
| ); | |||||
| return image != null ? File(image.path) : null; | |||||
| } on PlatformException catch (e) { | |||||
| throw ImagePickerException('Failed to pick image: ${e.message}'); | |||||
| } | |||||
| } | |||||
| /// Capturer une image avec la caméra | |||||
| Future<File?> captureImageFromCamera() async { | |||||
| try { | |||||
| final XFile? photo = await _picker.pickImage( | |||||
| source: ImageSource.camera, | |||||
| maxWidth: 1920, | |||||
| maxHeight: 1920, | |||||
| imageQuality: 90, | |||||
| ); | |||||
| return photo != null ? File(photo.path) : null; | |||||
| } on PlatformException catch (e) { | |||||
| throw ImagePickerException('Failed to capture image: ${e.message}'); | |||||
| } | |||||
| } | |||||
| /// Sélectionner une vidéo depuis la galerie | |||||
| Future<File?> pickVideoFromGallery() async { | |||||
| try { | |||||
| final XFile? video = await _picker.pickVideo( | |||||
| source: ImageSource.gallery, | |||||
| maxDuration: const Duration(minutes: 2), | |||||
| ); | |||||
| return video != null ? File(video.path) : null; | |||||
| } on PlatformException catch (e) { | |||||
| throw ImagePickerException('Failed to pick video: ${e.message}'); | |||||
| } | |||||
| } | |||||
| /// Capturer une vidéo avec la caméra | |||||
| Future<File?> captureVideoFromCamera() async { | |||||
| try { | |||||
| final XFile? video = await _picker.pickVideo( | |||||
| source: ImageSource.camera, | |||||
| maxDuration: const Duration(minutes: 2), | |||||
| ); | |||||
| return video != null ? File(video.path) : null; | |||||
| } on PlatformException catch (e) { | |||||
| throw ImagePickerException('Failed to capture video: ${e.message}'); | |||||
| } | |||||
| } | |||||
| /// Sauvegarder un fichier dans le dossier documents | |||||
| Future<File> saveToAppDirectory(File sourceFile, String fileName) async { | |||||
| try { | |||||
| final directory = await getApplicationDocumentsDirectory(); | |||||
| final filePath = path.join(directory.path, fileName); | |||||
| return await sourceFile.copy(filePath); | |||||
| } catch (e) { | |||||
| throw ImagePickerException('Failed to save file: $e'); | |||||
| } | |||||
| } | |||||
| /// Obtenir la taille d'un fichier en MB | |||||
| double getFileSizeInMB(File file) { | |||||
| return file.lengthSync() / (1024 * 1024); | |||||
| } | |||||
| } | |||||
| /// Exception personnalisée pour ImageService | |||||
| class ImagePickerException implements Exception { | |||||
| final String message; | |||||
| ImagePickerException(this.message); | |||||
| @override | |||||
| String toString() => 'ImagePickerException: $message'; | |||||
| } |
| // lib/main.dart | |||||
| import 'dart:io'; | |||||
| import 'package:flutter/material.dart'; | |||||
| // --- IMPORTS MANQUANTS AJOUTÉS --- | |||||
| import 'package:social_content_creator/repositories/ai_repository.dart'; | |||||
| // Les autres imports d'écrans sont corrects | |||||
| import 'package:social_content_creator/presentation/screens/ai_enhancement/ai_enhancement_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/export/export_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/home/login_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/home/home_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/image_preview/image_preview_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/media_picker/media_picker_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/post_preview/post_preview_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/post_refinement/post_refinement_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/profile_setup/profile_setup_screen.dart'; | |||||
| import 'package:social_content_creator/presentation/screens/text_generation/text_generation_screen.dart'; | |||||
| import 'package:social_content_creator/routes/app_routes.dart'; | |||||
| void main() { | |||||
| runApp(const MyApp()); | |||||
| } | |||||
| class MyApp extends StatelessWidget { | |||||
| const MyApp({super.key}); | |||||
| static const bool _devSkipLogin = true; | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| final Color seedColor = Colors.blue; | |||||
| return MaterialApp( | |||||
| title: 'Social Content Creator', | |||||
| theme: ThemeData( | |||||
| colorScheme: ColorScheme.fromSeed( | |||||
| seedColor: seedColor, | |||||
| brightness: Brightness.light, | |||||
| ), | |||||
| useMaterial3: true, | |||||
| ), | |||||
| darkTheme: ThemeData( | |||||
| colorScheme: ColorScheme.fromSeed( | |||||
| seedColor: seedColor, | |||||
| brightness: Brightness.dark, | |||||
| ), | |||||
| useMaterial3: true, | |||||
| ), | |||||
| themeMode: ThemeMode.system, | |||||
| debugShowCheckedModeBanner: false, | |||||
| initialRoute: _devSkipLogin ? AppRoutes.home : AppRoutes.login, | |||||
| // --- GESTIONNAIRE DE ROUTES ENTIÈREMENT CORRIGÉ ET COMPLÉTÉ --- | |||||
| onGenerateRoute: (settings) { | |||||
| switch (settings.name) { | |||||
| // --- Routes simples sans arguments --- | |||||
| case '/': | |||||
| case AppRoutes.login: | |||||
| return MaterialPageRoute(builder: (_) => const LoginScreen()); | |||||
| case AppRoutes.home: | |||||
| return MaterialPageRoute(builder: (_) => const HomeScreen()); | |||||
| case AppRoutes.profileSetup: | |||||
| return MaterialPageRoute(builder: (_) => const ProfileSetupScreen()); | |||||
| case AppRoutes.mediaPicker: | |||||
| return MaterialPageRoute(builder: (_) => const MediaPickerScreen()); | |||||
| case AppRoutes.export: | |||||
| return MaterialPageRoute(builder: (_) => const ExportScreen()); | |||||
| // --- Routes avec arguments --- | |||||
| case AppRoutes.aiEnhancement: | |||||
| if (settings.arguments is Map<String, dynamic>) { | |||||
| final args = settings.arguments as Map<String, dynamic>; | |||||
| if (args.containsKey('image') && | |||||
| args['image'] is File && | |||||
| args.containsKey('prompt') && | |||||
| args['prompt'] is String) { | |||||
| return MaterialPageRoute( | |||||
| builder: (_) => AiEnhancementScreen( | |||||
| image: args['image']!, | |||||
| prompt: args['prompt']!, | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } | |||||
| return _errorRoute("Arguments invalides pour AiEnhancementScreen."); | |||||
| case AppRoutes.imagePreview: | |||||
| if (settings.arguments is String) { | |||||
| final imageBase64 = settings.arguments as String; | |||||
| return MaterialPageRoute( | |||||
| builder: (_) => ImagePreviewScreen(imageBase64: imageBase64), | |||||
| ); | |||||
| } | |||||
| return _errorRoute("Argument (String) invalide pour ImagePreviewScreen"); | |||||
| case AppRoutes.textGeneration: | |||||
| final args = settings.arguments; | |||||
| if (args is Map<String, dynamic>) { | |||||
| final imageBase64 = args['imageBase64'] as String?; | |||||
| final aiRepository = args['aiRepository'] as AiRepository?; | |||||
| if (imageBase64 != null && aiRepository != null) { | |||||
| return MaterialPageRoute( | |||||
| builder: (_) => TextGenerationScreen( | |||||
| arguments: TextGenerationScreenArguments( | |||||
| imageBase64: imageBase64, | |||||
| aiRepository: aiRepository, | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } | |||||
| // L'erreur était ici : il manquait le cas de fallback | |||||
| return _errorRoute("Arguments invalides pour TextGenerationScreen."); | |||||
| // --- ROUTE MANQUANTE AJOUTÉE --- | |||||
| case AppRoutes.postRefinement: | |||||
| if (settings.arguments is PostRefinementScreenArguments) { | |||||
| final args = settings.arguments as PostRefinementScreenArguments; | |||||
| return MaterialPageRoute( | |||||
| builder: (_) => PostRefinementScreen(arguments: args), | |||||
| ); | |||||
| } | |||||
| return _errorRoute("Arguments (PostRefinementScreenArguments) invalides pour PostRefinementScreen."); | |||||
| // --- ROUTE MANQUANTE AJOUTÉE --- | |||||
| case AppRoutes.postPreview: | |||||
| if (settings.arguments is PostPreviewArguments) { | |||||
| final args = settings.arguments as PostPreviewArguments; | |||||
| return MaterialPageRoute( | |||||
| builder: (_) => PostPreviewScreen(arguments: args), | |||||
| ); | |||||
| } | |||||
| return _errorRoute("Arguments (PostPreviewArguments) invalides pour PostPreviewScreen."); | |||||
| default: | |||||
| return _errorRoute("Route non trouvée: ${settings.name}"); | |||||
| } | |||||
| }, | |||||
| ); | |||||
| } | |||||
| // Méthode helper pour afficher une page d'erreur propre | |||||
| MaterialPageRoute _errorRoute(String message) { | |||||
| return MaterialPageRoute( | |||||
| builder: (_) => Scaffold( | |||||
| appBar: AppBar(title: const Text('Erreur de Navigation')), | |||||
| body: Center( | |||||
| child: Padding( | |||||
| padding: const EdgeInsets.all(16.0), | |||||
| child: Text(message, textAlign: TextAlign.center), | |||||
| )), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| 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(), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| }), | |||||
| ); | |||||
| } | |||||
| } |
| import 'package:flutter/material.dart'; | |||||
| final class ExportScreen extends StatelessWidget { | |||||
| const ExportScreen({super.key}); | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return Scaffold( | |||||
| appBar: AppBar(title: const Text('Partager')), | |||||
| body: Center( | |||||
| child: Column( | |||||
| mainAxisAlignment: MainAxisAlignment.center, | |||||
| children: [ | |||||
| const Icon(Icons.share, size: 64), | |||||
| const SizedBox(height: 24), | |||||
| const Text('Sélectionnez les réseaux'), | |||||
| const SizedBox(height: 32), | |||||
| FilledButton( | |||||
| onPressed: () => ScaffoldMessenger.of(context).showSnackBar( | |||||
| const SnackBar(content: Text('Partage réussi!')), | |||||
| ), | |||||
| child: const Text('Partager maintenant'), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| import 'package:flutter/material.dart'; | |||||
| import 'package:social_content_creator/routes/app_routes.dart'; | |||||
| class HomeScreen extends StatelessWidget { | |||||
| const HomeScreen({super.key}); | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return Scaffold( | |||||
| appBar: AppBar( | |||||
| automaticallyImplyLeading: false, | |||||
| title: const Text("Accueil"), | |||||
| // Optionnel : Ajoutez un bouton de déconnexion | |||||
| actions: [ | |||||
| IconButton( | |||||
| icon: const Icon(Icons.logout), | |||||
| tooltip: "Déconnexion", | |||||
| onPressed: () { | |||||
| // Navigue vers l'écran de login et supprime toutes les routes précédentes | |||||
| Navigator.of(context).pushNamedAndRemoveUntil( | |||||
| AppRoutes.login, | |||||
| (Route<dynamic> route) => false | |||||
| ); | |||||
| }, | |||||
| ) | |||||
| ], | |||||
| ), | |||||
| body: Center( | |||||
| child: Column( | |||||
| mainAxisAlignment: MainAxisAlignment.center, | |||||
| children: [ | |||||
| const Text( | |||||
| "Vous êtes connecté !", | |||||
| style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), | |||||
| ), | |||||
| const SizedBox(height: 40), | |||||
| ElevatedButton.icon( | |||||
| icon: const Icon(Icons.add_photo_alternate_outlined), | |||||
| label: const Text("Commencer une nouvelle publication"), | |||||
| onPressed: () { | |||||
| // Lance le parcours de création de post existant | |||||
| Navigator.of(context).pushNamed(AppRoutes.mediaPicker); | |||||
| }, | |||||
| style: ElevatedButton.styleFrom( | |||||
| padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), | |||||
| ), | |||||
| ) | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| import 'package:flutter/material.dart'; | |||||
| import '../../../data/services/auth_service.dart'; // Assurez-vous que le chemin est correct | |||||
| class LoginScreen extends StatefulWidget { | |||||
| const LoginScreen({super.key}); | |||||
| @override | |||||
| State<LoginScreen> createState() => _LoginScreenState(); | |||||
| } | |||||
| class _LoginScreenState extends State<LoginScreen> { | |||||
| final AuthService _authService = AuthService(); | |||||
| bool _isLoading = false; | |||||
| void _handleFacebookLogin() async { | |||||
| setState(() { | |||||
| _isLoading = true; | |||||
| }); | |||||
| final bool success = await _authService.loginWithFacebook(); | |||||
| setState(() { | |||||
| _isLoading = false; | |||||
| }); | |||||
| if (success && mounted) { | |||||
| // Naviguer vers l'écran principal de l'application | |||||
| // Par exemple : | |||||
| Navigator.of(context).pushReplacementNamed('/home'); | |||||
| } else if (mounted) { | |||||
| // Afficher un message d'erreur | |||||
| ScaffoldMessenger.of(context).showSnackBar( | |||||
| const SnackBar(content: Text("La connexion a échoué. Veuillez réessayer.")), | |||||
| ); | |||||
| } | |||||
| } | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return Scaffold( | |||||
| appBar: AppBar(title: const Text("Connexion")), | |||||
| body: Center( | |||||
| child: _isLoading | |||||
| ? const CircularProgressIndicator() | |||||
| : ElevatedButton.icon( | |||||
| icon: const Icon(Icons.facebook), | |||||
| label: const Text("Se connecter avec Facebook"), | |||||
| onPressed: _handleFacebookLogin, | |||||
| style: ElevatedButton.styleFrom( | |||||
| backgroundColor: const Color(0xFF1877F2), // Couleur de Facebook | |||||
| foregroundColor: Colors.white, | |||||
| padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| // lib/presentation/screens/image_preview/image_preview_screen.dart | |||||
| import 'dart:convert'; | |||||
| import 'package:flutter/material.dart'; | |||||
| // --- CORRECTION 1 : IMPORTER SEULEMENT CE QUI EST NÉCESSAIRE --- | |||||
| import '../../../repositories/ai_repository.dart'; | |||||
| import '../../../routes/app_routes.dart'; | |||||
| // L'import de 'ollama_service.dart' est supprimé. | |||||
| // --- CORRECTION 2 : SIMPLIFIER LE CONSTRUCTEUR --- | |||||
| // Cet écran n'a plus besoin de recevoir un service, car l'écran suivant | |||||
| // instanciera lui-même le AiRepository dont il a besoin. | |||||
| final class ImagePreviewScreen extends StatelessWidget { | |||||
| final String imageBase64; | |||||
| const ImagePreviewScreen({super.key, required this.imageBase64}); | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| // Le AiRepository est instancié ici, uniquement si on en a besoin pour la navigation. | |||||
| // Dans ce cas, on le passe à l'écran suivant. | |||||
| final AiRepository aiRepository = AiRepository(); | |||||
| try { | |||||
| final imageBytes = base64Decode(imageBase64); | |||||
| return Scaffold( | |||||
| appBar: AppBar( | |||||
| title: const Text("Aperçu de l'image"), | |||||
| backgroundColor: Colors.black, | |||||
| elevation: 0, | |||||
| ), | |||||
| backgroundColor: Colors.black, | |||||
| body: SafeArea( | |||||
| child: Center( | |||||
| child: InteractiveViewer( | |||||
| panEnabled: true, | |||||
| minScale: 0.5, | |||||
| maxScale: 4.0, | |||||
| child: Image.memory( | |||||
| imageBytes, | |||||
| fit: BoxFit.contain, | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| floatingActionButton: FloatingActionButton.extended( | |||||
| // --- CORRECTION 3 : SIMPLIFIER LA LOGIQUE DE NAVIGATION --- | |||||
| onPressed: () { | |||||
| // On navigue vers l'écran suivant en lui passant les arguments nécessaires. | |||||
| // L'écran 'TextGeneration' aura besoin de l'image et du repository pour travailler. | |||||
| Navigator.pushNamed( | |||||
| context, | |||||
| AppRoutes.textGeneration, // La route vers l'écran de génération de texte | |||||
| arguments: { | |||||
| 'imageBase64': imageBase64, | |||||
| 'aiRepository': aiRepository, | |||||
| }, | |||||
| ); | |||||
| }, | |||||
| label: const Text('Utiliser cette image'), | |||||
| icon: const Icon(Icons.check), | |||||
| ), | |||||
| ); | |||||
| } catch (e) { | |||||
| // Le bloc catch reste inchangé, il est correct. | |||||
| return Scaffold( | |||||
| appBar: AppBar(title: const Text('Erreur')), | |||||
| body: const Center( | |||||
| child: Text( | |||||
| 'Erreur : aucune image à afficher. Les données sont peut-être corrompues.', | |||||
| textAlign: TextAlign.center, | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } | |||||
| } |
| // lib/presentation/screens/media_picker/media_picker_screen.dart | |||||
| import 'dart:convert'; // <<< NÉCESSAIRE POUR BASE64 | |||||
| import 'dart:io'; | |||||
| import 'package:flutter/material.dart'; | |||||
| import 'package:image_picker/image_picker.dart'; | |||||
| import 'package:social_content_creator/routes/app_routes.dart'; | |||||
| import 'package:social_content_creator/services/image_analysis_service.dart'; // <<< IMPORT CORRECT | |||||
| class MediaPickerScreen extends StatefulWidget { | |||||
| const MediaPickerScreen({super.key}); | |||||
| @override | |||||
| State<MediaPickerScreen> createState() => _MediaPickerScreenState(); | |||||
| } | |||||
| class _MediaPickerScreenState extends State<MediaPickerScreen> { | |||||
| final _picker = ImagePicker(); | |||||
| XFile? _selectedMedia; | |||||
| // On instancie la classe CONCRÈTE | |||||
| final ImageAnalysisService _analysisService = OllamaImageAnalysisService(); | |||||
| bool _isAnalyzing = false; | |||||
| Future<void> _pickImage(ImageSource source) async { | |||||
| try { | |||||
| final file = await _picker.pickImage(source: source, imageQuality: 85, maxWidth: 1280); | |||||
| if (file != null) { | |||||
| setState(() => _selectedMedia = file); | |||||
| _analyzeAndNavigate(); | |||||
| } | |||||
| } catch (e) { | |||||
| if (!mounted) return; | |||||
| ScaffoldMessenger.of(context).showSnackBar( | |||||
| SnackBar(content: Text("Erreur lors de la sélection de l'image: $e")), | |||||
| ); | |||||
| } | |||||
| } | |||||
| /// Fonction qui analyse l'image et navigue vers l'écran suivant. | |||||
| Future<void> _analyzeAndNavigate() async { | |||||
| if (_selectedMedia == null) return; | |||||
| setState(() => _isAnalyzing = true); | |||||
| try { | |||||
| final imageFile = File(_selectedMedia!.path); | |||||
| // --- CORRECTION APPLIQUÉE ICI --- | |||||
| // 1. Convertir l'image en base64, comme attendu par le service. | |||||
| final imageBytes = await imageFile.readAsBytes(); | |||||
| final String imageBase64 = base64Encode(imageBytes); | |||||
| // 2. Appeler la bonne méthode (`analyzeImage`) avec le bon argument (`base64Image`). | |||||
| final String imagePrompt = await _analysisService.analyzeImage(imageBase64); | |||||
| // --- FIN DE LA CORRECTION --- | |||||
| if (!mounted) return; | |||||
| // 3. Navigation avec l'image et le prompt | |||||
| Navigator.pushNamed( | |||||
| context, | |||||
| AppRoutes.aiEnhancement, | |||||
| arguments: <String, dynamic>{ | |||||
| 'image': imageFile, | |||||
| 'prompt': imagePrompt, | |||||
| }, | |||||
| ); | |||||
| } catch (e) { | |||||
| if (!mounted) return; | |||||
| ScaffoldMessenger.of(context).showSnackBar( | |||||
| SnackBar(content: Text("Erreur lors de l'analyse de l'image : $e")), | |||||
| ); | |||||
| } finally { | |||||
| if (mounted) { | |||||
| setState(() => _isAnalyzing = false); | |||||
| } | |||||
| } | |||||
| } | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return Scaffold( | |||||
| appBar: AppBar(title: const Text('1. Choisir une Image')), | |||||
| body: Stack( | |||||
| children: [ | |||||
| SafeArea( | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.stretch, | |||||
| children: [ | |||||
| Expanded( | |||||
| child: Center( | |||||
| child: Padding( | |||||
| padding: const EdgeInsets.all(24.0), | |||||
| child: Column( | |||||
| mainAxisSize: MainAxisSize.min, | |||||
| crossAxisAlignment: CrossAxisAlignment.stretch, | |||||
| children: [ | |||||
| const Icon(Icons.camera_enhance, size: 80, color: Colors.grey), | |||||
| const SizedBox(height: 24), | |||||
| Text( | |||||
| "Choisissez une image pour commencer", | |||||
| style: Theme.of(context).textTheme.headlineSmall, | |||||
| textAlign: TextAlign.center, | |||||
| ), | |||||
| const SizedBox(height: 48), | |||||
| FilledButton.icon( | |||||
| onPressed: _isAnalyzing ? null : () => _pickImage(ImageSource.gallery), | |||||
| icon: const Icon(Icons.photo_library_outlined), | |||||
| label: const Text('Importer depuis la galerie'), | |||||
| style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), | |||||
| ), | |||||
| const SizedBox(height: 12), | |||||
| OutlinedButton.icon( | |||||
| onPressed: _isAnalyzing ? null : () => _pickImage(ImageSource.camera), | |||||
| icon: const Icon(Icons.camera_alt_outlined), | |||||
| label: const Text('Prendre une photo'), | |||||
| style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| if (_isAnalyzing) | |||||
| Container( | |||||
| color: Colors.black.withOpacity(0.5), | |||||
| child: const Center( | |||||
| child: Column( | |||||
| mainAxisSize: MainAxisSize.min, | |||||
| children: [ | |||||
| CircularProgressIndicator(), | |||||
| SizedBox(height: 16), | |||||
| Text("Analyse de l'image...", style: TextStyle(color: Colors.white, fontSize: 16)), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| // lib/presentation/screens/post_preview/post_preview_screen.dart | |||||
| import 'dart:convert'; | |||||
| import 'package:flutter/material.dart'; | |||||
| import '../../../repositories/ai_repository.dart'; | |||||
| import '../../widgets/creation_flow_layout.dart'; | |||||
| // --- ACTION 1 : CRÉER LA CLASSE D'ARGUMENTS MANQUANTE --- | |||||
| // Cette classe encapsule toutes les données nécessaires pour cet écran. | |||||
| class PostPreviewArguments { | |||||
| final String imageBase64; | |||||
| final String text; | |||||
| final AiRepository aiRepository; | |||||
| PostPreviewArguments({ | |||||
| required this.imageBase64, | |||||
| required this.text, | |||||
| required this.aiRepository, | |||||
| }); | |||||
| } | |||||
| // --- ACTION 2 : ADAPTER L'ÉCRAN POUR UTILISER LA CLASSE D'ARGUMENTS --- | |||||
| final class PostPreviewScreen extends StatelessWidget { | |||||
| // L'écran attend maintenant un seul objet 'arguments' | |||||
| final PostPreviewArguments arguments; | |||||
| const PostPreviewScreen({ | |||||
| super.key, | |||||
| required this.arguments, // Le constructeur est simplifié | |||||
| }); | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return CreationFlowLayout( | |||||
| // Adaptez ce chiffre au nombre total d'étapes de votre flux. | |||||
| currentStep: 6, | |||||
| title: "Aperçu & Publication", | |||||
| child: SingleChildScrollView( | |||||
| padding: const EdgeInsets.all(16.0), | |||||
| child: Column( | |||||
| children: [ | |||||
| // Widget de carte simulant un post de réseau social | |||||
| Card( | |||||
| clipBehavior: Clip.antiAlias, | |||||
| elevation: 4, | |||||
| shape: RoundedRectangleBorder( | |||||
| borderRadius: BorderRadius.circular(12), | |||||
| ), | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.start, | |||||
| children: [ | |||||
| // L'image du post | |||||
| Image.memory( | |||||
| // On accède aux données via l'objet 'arguments' | |||||
| base64Decode(arguments.imageBase64), | |||||
| width: double.infinity, | |||||
| height: 350, | |||||
| fit: BoxFit.cover, | |||||
| ), | |||||
| // Le texte final du post | |||||
| Padding( | |||||
| padding: const EdgeInsets.all(16.0), | |||||
| child: Text( | |||||
| // On accède aux données via l'objet 'arguments' | |||||
| arguments.text, | |||||
| style: const TextStyle(fontSize: 16), | |||||
| ), | |||||
| ), | |||||
| // Barre d'actions (inchangée) | |||||
| Padding( | |||||
| padding: const EdgeInsets.symmetric( | |||||
| horizontal: 16.0, vertical: 8.0), | |||||
| child: Row( | |||||
| mainAxisAlignment: MainAxisAlignment.spaceAround, | |||||
| children: [ | |||||
| Icon(Icons.favorite_border, | |||||
| color: Theme.of(context) | |||||
| .colorScheme | |||||
| .onSurface | |||||
| .withOpacity(0.6)), | |||||
| Icon(Icons.mode_comment_outlined, | |||||
| color: Theme.of(context) | |||||
| .colorScheme | |||||
| .onSurface | |||||
| .withOpacity(0.6)), | |||||
| Icon(Icons.send_outlined, | |||||
| color: Theme.of(context) | |||||
| .colorScheme | |||||
| .onSurface | |||||
| .withOpacity(0.6)), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| const SizedBox(height: 24), | |||||
| // Bouton final de publication | |||||
| SizedBox( | |||||
| width: double.infinity, | |||||
| child: FilledButton.icon( | |||||
| onPressed: () { | |||||
| // La logique de publication utilise maintenant les arguments | |||||
| // arguments.aiRepository.publishPost( | |||||
| // image: arguments.imageBase64, | |||||
| // text: arguments.text, | |||||
| // ); | |||||
| ScaffoldMessenger.of(context).showSnackBar( | |||||
| const SnackBar( | |||||
| content: Text('Publication simulée avec succès !'), | |||||
| backgroundColor: Colors.green, | |||||
| ), | |||||
| ); | |||||
| // Potentiellement, naviguer vers l'accueil après publication | |||||
| // Navigator.of(context).popUntil((route) => route.isFirst); | |||||
| }, | |||||
| icon: const Icon(Icons.check_circle_outline), | |||||
| label: const Text('Publier maintenant'), | |||||
| style: FilledButton.styleFrom( | |||||
| padding: const EdgeInsets.symmetric(vertical: 16), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| // lib/presentation/screens/post_refinement/post_refinement_screen.dart | |||||
| import 'package:flutter/material.dart'; | |||||
| // --- CORRECTION 1 : IMPORTER LE REPOSITORY --- | |||||
| import '../../../repositories/ai_repository.dart'; | |||||
| import 'package:social_content_creator/routes/app_routes.dart'; | |||||
| import '../../widgets/creation_flow_layout.dart'; | |||||
| import '../post_preview/post_preview_screen.dart'; | |||||
| // L'import de 'ollama_service.dart' est supprimé. | |||||
| // --- CORRECTION 2 : DÉFINIR UNE CLASSE D'ARGUMENTS PROPRE --- | |||||
| class PostRefinementScreenArguments { | |||||
| final String initialText; | |||||
| final String imageBase64; | |||||
| final AiRepository aiRepository; | |||||
| PostRefinementScreenArguments({ | |||||
| required this.initialText, | |||||
| required this.imageBase64, | |||||
| required this.aiRepository, | |||||
| }); | |||||
| } | |||||
| class PostRefinementScreen extends StatefulWidget { | |||||
| // Le constructeur attend maintenant la classe d'arguments. | |||||
| final PostRefinementScreenArguments arguments; | |||||
| const PostRefinementScreen({ | |||||
| super.key, | |||||
| required this.arguments, | |||||
| }); | |||||
| @override | |||||
| State<PostRefinementScreen> createState() => _PostRefinementScreenState(); | |||||
| } | |||||
| class _PostRefinementScreenState extends State<PostRefinementScreen> { | |||||
| late final TextEditingController _postTextController; | |||||
| final TextEditingController _promptController = TextEditingController(); | |||||
| bool _isImproving = false; | |||||
| @override | |||||
| void initState() { | |||||
| super.initState(); | |||||
| // On initialise le texte depuis les arguments reçus. | |||||
| _postTextController = TextEditingController(text: widget.arguments.initialText); | |||||
| } | |||||
| @override | |||||
| void dispose() { | |||||
| _postTextController.dispose(); | |||||
| _promptController.dispose(); | |||||
| super.dispose(); | |||||
| } | |||||
| // --- CORRECTION 3 : UTILISER LE REPOSITORY POUR L'AMÉLIORATION --- | |||||
| Future<void> _handleImproveWithAI() async { | |||||
| final instruction = _promptController.text; | |||||
| if (instruction.isEmpty || _isImproving) return; | |||||
| if (!mounted) return; | |||||
| setState(() => _isImproving = true); | |||||
| try { | |||||
| // On appelle la méthode du Repository, qui se chargera de déléguer au bon service. | |||||
| final improvedText = await widget.arguments.aiRepository.improvePostText( | |||||
| originalText: _postTextController.text, | |||||
| userInstruction: instruction, | |||||
| ); | |||||
| if (mounted) { | |||||
| setState(() { | |||||
| _postTextController.text = improvedText; | |||||
| _promptController.clear(); | |||||
| }); | |||||
| } | |||||
| } catch (e) { | |||||
| if (mounted) { | |||||
| ScaffoldMessenger.of(context).showSnackBar(SnackBar( | |||||
| content: Text("Erreur d'amélioration : ${e.toString()}"), | |||||
| backgroundColor: Colors.red)); | |||||
| } | |||||
| } finally { | |||||
| if (mounted) { | |||||
| setState(() => _isImproving = false); | |||||
| } | |||||
| } | |||||
| } | |||||
| // --- CORRECTION 4 : NAVIGATION PROPRE VERS L'APERÇU FINAL --- | |||||
| // --- CORRECTION APPLIQUÉE ICI --- | |||||
| void _navigateToPreview() { | |||||
| Navigator.pushNamed( | |||||
| context, | |||||
| AppRoutes.postPreview, | |||||
| // Au lieu d'envoyer une Map, on crée l'objet PostPreviewArguments | |||||
| // que l'écran suivant attend. | |||||
| arguments: PostPreviewArguments( | |||||
| imageBase64: widget.arguments.imageBase64, | |||||
| text: _postTextController.text, | |||||
| aiRepository: widget.arguments.aiRepository, | |||||
| ), | |||||
| ); | |||||
| } | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| // Le widget build est INCHANGÉ dans sa structure. | |||||
| return CreationFlowLayout( // Ajout du layout de flux | |||||
| currentStep: 5, // C'est la 6ème étape | |||||
| title: "5. Affinage du texte", | |||||
| child: Scaffold( | |||||
| appBar: AppBar( | |||||
| title: const Text('Affiner le post'), | |||||
| actions: [ | |||||
| FilledButton.tonal( | |||||
| onPressed: _navigateToPreview, | |||||
| child: const Text('Valider'), | |||||
| ), | |||||
| const SizedBox(width: 16), | |||||
| ], | |||||
| ), | |||||
| body: SingleChildScrollView( | |||||
| padding: const EdgeInsets.all(16.0), | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.stretch, | |||||
| children: [ | |||||
| TextField( | |||||
| controller: _postTextController, | |||||
| maxLines: 8, | |||||
| decoration: const InputDecoration( | |||||
| labelText: 'Texte du post', | |||||
| border: OutlineInputBorder(), | |||||
| alignLabelWithHint: true, | |||||
| ), | |||||
| ), | |||||
| const SizedBox(height: 24), | |||||
| const Row(children: [ | |||||
| Expanded(child: Divider()), | |||||
| Padding( | |||||
| padding: EdgeInsets.symmetric(horizontal: 8.0), | |||||
| child: Text("Améliorer avec l'IA"), | |||||
| ), | |||||
| Expanded(child: Divider()), | |||||
| ]), | |||||
| const SizedBox(height: 16), | |||||
| TextField( | |||||
| controller: _promptController, | |||||
| decoration: InputDecoration( | |||||
| hintText: "Ex: ajoute plus de détails, rends-le plus fun...", | |||||
| border: const OutlineInputBorder(), | |||||
| ), | |||||
| ), | |||||
| const SizedBox(height: 16), | |||||
| FilledButton.icon( | |||||
| onPressed: _handleImproveWithAI, | |||||
| icon: _isImproving | |||||
| ? const SizedBox( | |||||
| width: 20, | |||||
| height: 20, | |||||
| child: CircularProgressIndicator( | |||||
| color: Colors.white, strokeWidth: 2)) | |||||
| : const Icon(Icons.auto_awesome), | |||||
| label: const Text("Lancer l'amélioration"), | |||||
| style: FilledButton.styleFrom( | |||||
| padding: const EdgeInsets.symmetric(vertical: 16)), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| import 'package:flutter/material.dart'; | |||||
| import '../../../core/theme/colors.dart'; | |||||
| import '../../../data/models/user_profile.dart'; | |||||
| // L'import des routes n'est plus nécessaire ici pour la navigation, mais on le garde pour la propreté. | |||||
| import 'package:social_content_creator/routes/app_routes.dart'; | |||||
| final class ProfileSetupScreen extends StatefulWidget { | |||||
| const ProfileSetupScreen({super.key}); | |||||
| @override | |||||
| State<ProfileSetupScreen> createState() => _ProfileSetupScreenState(); | |||||
| } | |||||
| class _ProfileSetupScreenState extends State<ProfileSetupScreen> { | |||||
| final List<UserProfile> _profiles = []; | |||||
| @override | |||||
| void initState() { | |||||
| super.initState(); | |||||
| _addProfile(); | |||||
| } | |||||
| void _addProfile() { | |||||
| setState(() { | |||||
| _profiles.add( | |||||
| const UserProfile( | |||||
| profession: '', | |||||
| tone: MessageTone.normal, | |||||
| textStyle: TextStyleEnum.classic, | |||||
| ), | |||||
| ); | |||||
| }); | |||||
| } | |||||
| void _updateProfile(int index, UserProfile profile) { | |||||
| setState(() => _profiles[index] = profile); | |||||
| } | |||||
| void _removeProfile(int index) { | |||||
| if (_profiles.length > 1) { | |||||
| setState(() => _profiles.removeAt(index)); | |||||
| } | |||||
| } | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return Scaffold( | |||||
| appBar: AppBar(title: const Text('Créer votre profil')), | |||||
| body: SafeArea( | |||||
| child: Column( | |||||
| children: [ | |||||
| Expanded( | |||||
| child: SingleChildScrollView( | |||||
| padding: const EdgeInsets.all(20), | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.start, | |||||
| children: [ | |||||
| Text( | |||||
| 'Configurez vos profils', | |||||
| style: Theme.of(context).textTheme.headlineSmall, | |||||
| ), | |||||
| const SizedBox(height: 24), | |||||
| ..._profiles.asMap().entries.map((e) { | |||||
| final (index, profile) = (e.key, e.value); | |||||
| return _ProfileCard( | |||||
| profile: profile, | |||||
| onUpdate: (p) => _updateProfile(index, p), | |||||
| onRemove: _profiles.length > 1 | |||||
| ? () => _removeProfile(index) | |||||
| : null, | |||||
| index: index, | |||||
| ); | |||||
| }), | |||||
| if (_profiles.length < 5) ...[ | |||||
| const SizedBox(height: 16), | |||||
| OutlinedButton.icon( | |||||
| onPressed: _addProfile, | |||||
| icon: const Icon(Icons.add), | |||||
| label: const Text('Ajouter un profil'), | |||||
| ), | |||||
| ], | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| Container( | |||||
| padding: const EdgeInsets.all(20), | |||||
| child: SizedBox( | |||||
| width: double.infinity, | |||||
| child: FilledButton( | |||||
| onPressed: _profiles.any((p) => p.profession.isNotEmpty) | |||||
| ? () { | |||||
| // --- CORRECTION --- | |||||
| // 1. Logique pour sauvegarder les profils (à ajouter si nécessaire) | |||||
| // | |||||
| // 2. On ferme simplement l'écran de configuration pour revenir | |||||
| // à l'écran qui l'a appelé (HomeScreen). | |||||
| Navigator.pop(context); | |||||
| } | |||||
| : null, | |||||
| child: const Text('Enregistrer et Terminer'), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } | |||||
| // ... Le widget _ProfileCard reste inchangé ... | |||||
| class _ProfileCard extends StatefulWidget { | |||||
| final UserProfile profile; | |||||
| final Function(UserProfile) onUpdate; | |||||
| final VoidCallback? onRemove; | |||||
| final int index; | |||||
| const _ProfileCard({ | |||||
| required this.profile, | |||||
| required this.onUpdate, | |||||
| this.onRemove, | |||||
| required this.index, | |||||
| }); | |||||
| @override | |||||
| State<_ProfileCard> createState() => _ProfileCardState(); | |||||
| } | |||||
| class _ProfileCardState extends State<_ProfileCard> { | |||||
| late final TextEditingController _controller; | |||||
| @override | |||||
| void initState() { | |||||
| super.initState(); | |||||
| _controller = TextEditingController(text: widget.profile.profession); | |||||
| } | |||||
| @override | |||||
| void didUpdateWidget(_ProfileCard oldWidget) { | |||||
| super.didUpdateWidget(oldWidget); | |||||
| if (widget.profile.profession != _controller.text) { | |||||
| _controller.text = widget.profile.profession; | |||||
| } | |||||
| } | |||||
| @override | |||||
| void dispose() { | |||||
| _controller.dispose(); | |||||
| super.dispose(); | |||||
| } | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return Card( | |||||
| margin: const EdgeInsets.only(bottom: 16), | |||||
| child: Padding( | |||||
| padding: const EdgeInsets.all(16), | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.start, | |||||
| children: [ | |||||
| Row( | |||||
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |||||
| children: [ | |||||
| Text('Profil ${widget.index + 1}'), | |||||
| if (widget.onRemove != null) | |||||
| IconButton(icon: const Icon(Icons.close), onPressed: widget.onRemove), | |||||
| ], | |||||
| ), | |||||
| const SizedBox(height: 16), | |||||
| TextField( | |||||
| controller: _controller, | |||||
| decoration: const InputDecoration(labelText: 'Métier'), | |||||
| onChanged: (v) => widget.onUpdate(widget.profile.copyWith(profession: v)), | |||||
| ), | |||||
| const SizedBox(height: 16), | |||||
| Text('Ton:', style: Theme.of(context).textTheme.bodySmall), | |||||
| Wrap( | |||||
| spacing: 8, | |||||
| children: MessageTone.values.map((t) { | |||||
| return FilterChip( | |||||
| label: Text(t.displayName), | |||||
| selected: widget.profile.tone == t, | |||||
| onSelected: (_) => widget.onUpdate(widget.profile.copyWith(tone: t)), | |||||
| ); | |||||
| }).toList(), | |||||
| ), | |||||
| const SizedBox(height: 16), | |||||
| Text('Style:', style: Theme.of(context).textTheme.bodySmall), | |||||
| Wrap( | |||||
| spacing: 8, | |||||
| children: TextStyleEnum.values.map((s) { | |||||
| return FilterChip( | |||||
| label: Text(s.displayName), | |||||
| selected: widget.profile.textStyle == s, | |||||
| onSelected: (_) => widget.onUpdate(widget.profile.copyWith(textStyle: s)), | |||||
| ); | |||||
| }).toList(), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| // lib/presentation/screens/text_generation/text_generation_screen.dart | |||||
| import 'dart:convert'; | |||||
| import 'package:flutter/material.dart';// --- CORRECTION 1 : IMPORTER LE REPOSITORY --- | |||||
| import '../../../repositories/ai_repository.dart'; | |||||
| import '../../../routes/app_routes.dart'; | |||||
| import '../../widgets/creation_flow_layout.dart'; | |||||
| import '../post_refinement/post_refinement_screen.dart'; // Import pour la classe d'arguments | |||||
| // --- CORRECTION 2 : DÉFINIR UNE CLASSE D'ARGUMENTS PROPRE --- | |||||
| class TextGenerationScreenArguments { | |||||
| final String imageBase64; | |||||
| final AiRepository aiRepository; | |||||
| TextGenerationScreenArguments({ | |||||
| required this.imageBase64, | |||||
| required this.aiRepository, | |||||
| }); | |||||
| } | |||||
| final class TextGenerationScreen extends StatefulWidget { | |||||
| // Le constructeur attend maintenant la classe d'arguments. | |||||
| final TextGenerationScreenArguments arguments; | |||||
| const TextGenerationScreen({super.key, required this.arguments}); | |||||
| @override | |||||
| State<TextGenerationScreen> createState() => _TextGenerationScreenState(); | |||||
| } | |||||
| class _TextGenerationScreenState extends State<TextGenerationScreen> { | |||||
| // Les variables d'état et contrôleurs restent inchangés | |||||
| bool _loading = false; | |||||
| List<String> _generatedIdeas = []; | |||||
| final List<String> _logs = []; | |||||
| final _professionController = TextEditingController(text: 'Entrepreneur digital'); | |||||
| final _toneController = TextEditingController(text: 'Professionnel et engageant'); | |||||
| @override | |||||
| void initState() { | |||||
| super.initState(); | |||||
| _addLog("🖼️ Image reçue avec succès."); | |||||
| } | |||||
| void _addLog(String log) { | |||||
| if (mounted) { | |||||
| setState(() => _logs.insert(0, log)); | |||||
| } | |||||
| } | |||||
| @override | |||||
| void dispose() { | |||||
| _professionController.dispose(); | |||||
| _toneController.dispose(); | |||||
| super.dispose(); | |||||
| } | |||||
| // --- CORRECTION 3 : UTILISER LE REPOSITORY POUR LA GÉNÉRATION --- | |||||
| Future<void> _handleGenerateIdeas() async { | |||||
| if (_loading) { | |||||
| _addLog("⏳ Annulation : Génération déjà en cours."); | |||||
| return; | |||||
| } | |||||
| if (!mounted) return; | |||||
| setState(() { | |||||
| _loading = true; | |||||
| _generatedIdeas = []; | |||||
| _logs.clear(); | |||||
| }); | |||||
| _addLog("▶️ Lancement de la génération d'idées..."); | |||||
| try { | |||||
| final profession = _professionController.text; | |||||
| final tone = _toneController.text; | |||||
| _addLog("⚙️ Paramètres : Métier='${profession}', Ton='${tone}'."); | |||||
| _addLog("🚀 Appel du AiRepository..."); | |||||
| // On appelle la méthode du Repository, qui délègue au bon service. | |||||
| final ideas = await widget.arguments.aiRepository.generatePostIdeas( | |||||
| base64Image: widget.arguments.imageBase64, | |||||
| profession: profession, | |||||
| tone: tone, | |||||
| ); | |||||
| if (!mounted) return; | |||||
| _addLog("✅ Succès ! Réponse reçue."); | |||||
| if (ideas.isEmpty) { | |||||
| _addLog("⚠️ Avertissement : Le service a retourné une liste vide."); | |||||
| } else { | |||||
| _addLog("📦 ${ideas.length} idée(s) reçue(s)."); | |||||
| } | |||||
| setState(() { | |||||
| _generatedIdeas = ideas; | |||||
| }); | |||||
| _addLog("✨ Interface mise à jour."); | |||||
| } catch (e) { | |||||
| if (!mounted) return; | |||||
| _addLog("❌ ERREUR : ${e.toString()}"); | |||||
| ScaffoldMessenger.of(context).showSnackBar(SnackBar( | |||||
| content: Text('Erreur: ${e.toString()}'), | |||||
| backgroundColor: Colors.red)); | |||||
| } finally { | |||||
| if (mounted) { | |||||
| setState(() => _loading = false); | |||||
| } | |||||
| _addLog("⏹️ Fin du processus."); | |||||
| } | |||||
| } | |||||
| // --- CORRECTION 4 : NAVIGATION PROPRE VERS L'ÉCRAN D'AFFINAGE --- | |||||
| void _navigateToRefinementScreen(String selectedIdea) { | |||||
| Navigator.pushNamed( | |||||
| context, | |||||
| AppRoutes.postRefinement, | |||||
| arguments: PostRefinementScreenArguments( | |||||
| initialText: selectedIdea, | |||||
| imageBase64: widget.arguments.imageBase64, | |||||
| aiRepository: widget.arguments.aiRepository, // On passe le Repository | |||||
| ), | |||||
| ); | |||||
| } | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| // La structure du build reste la même, mais je corrige l'index de l'étape. | |||||
| return CreationFlowLayout( | |||||
| currentStep: 4, // C'est la 5ème étape (index 4) | |||||
| title: '4. Génération de Texte', | |||||
| child: Stack( | |||||
| children: [ | |||||
| SingleChildScrollView( | |||||
| padding: const EdgeInsets.all(16.0), | |||||
| child: Column( | |||||
| crossAxisAlignment: CrossAxisAlignment.start, | |||||
| children: [ | |||||
| Center( | |||||
| child: ConstrainedBox( | |||||
| constraints: const BoxConstraints(maxHeight: 200), | |||||
| child: ClipRRect( | |||||
| borderRadius: BorderRadius.circular(12.0), | |||||
| child: Image.memory( | |||||
| base64Decode(widget.arguments.imageBase64), | |||||
| width: double.infinity, | |||||
| fit: BoxFit.cover, | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| const SizedBox(height: 24), | |||||
| TextField( | |||||
| controller: _professionController, | |||||
| decoration: const InputDecoration( | |||||
| labelText: 'Votre métier', | |||||
| border: OutlineInputBorder(), | |||||
| prefixIcon: Icon(Icons.work_outline))), | |||||
| const SizedBox(height: 16), | |||||
| TextField( | |||||
| controller: _toneController, | |||||
| decoration: const InputDecoration( | |||||
| labelText: 'Ton souhaité', | |||||
| border: OutlineInputBorder(), | |||||
| prefixIcon: Icon(Icons.campaign_outlined))), | |||||
| const SizedBox(height: 24), | |||||
| SizedBox( | |||||
| width: double.infinity, | |||||
| child: FilledButton.icon( | |||||
| onPressed: _loading ? null : _handleGenerateIdeas, | |||||
| icon: _loading | |||||
| ? const SizedBox( | |||||
| width: 20, | |||||
| height: 20, | |||||
| child: CircularProgressIndicator( | |||||
| color: Colors.white, strokeWidth: 2)) | |||||
| : const Icon(Icons.auto_awesome), | |||||
| label: const Text('Générer 3 idées de post'), | |||||
| style: FilledButton.styleFrom( | |||||
| padding: const EdgeInsets.symmetric(vertical: 16)), | |||||
| ), | |||||
| ), | |||||
| const SizedBox(height: 24), | |||||
| if (_generatedIdeas.isNotEmpty) ...[ | |||||
| const Divider(height: 32), | |||||
| Text("Choisissez une idée à affiner", | |||||
| style: Theme.of(context) | |||||
| .textTheme | |||||
| .titleMedium | |||||
| ?.copyWith(fontWeight: FontWeight.bold)), | |||||
| const SizedBox(height: 16), | |||||
| ListView.builder( | |||||
| shrinkWrap: true, | |||||
| physics: const NeverScrollableScrollPhysics(), | |||||
| itemCount: _generatedIdeas.length, | |||||
| itemBuilder: (context, index) { | |||||
| final idea = _generatedIdeas[index]; | |||||
| return Card( | |||||
| margin: const EdgeInsets.only(bottom: 12), | |||||
| child: ListTile( | |||||
| title: Text(idea, | |||||
| maxLines: 3, | |||||
| overflow: TextOverflow.ellipsis), | |||||
| leading: | |||||
| CircleAvatar(child: Text('${index + 1}')), | |||||
| trailing: const Icon(Icons.edit_note), | |||||
| onTap: () => _navigateToRefinementScreen(idea))); | |||||
| }, | |||||
| ), | |||||
| ], | |||||
| const SizedBox(height: 150), // Espace pour la console de logs | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| // La console de logs reste inchangée | |||||
| Positioned( | |||||
| bottom: 0, | |||||
| left: 0, | |||||
| right: 0, | |||||
| child: IgnorePointer( | |||||
| child: Container( | |||||
| height: 140, | |||||
| padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | |||||
| decoration: BoxDecoration( | |||||
| gradient: LinearGradient( | |||||
| colors: [ | |||||
| Theme.of(context).scaffoldBackgroundColor.withOpacity(0.0), | |||||
| Theme.of(context).scaffoldBackgroundColor.withOpacity(0.6), | |||||
| Theme.of(context).scaffoldBackgroundColor, | |||||
| ], | |||||
| begin: Alignment.topCenter, | |||||
| end: Alignment.bottomCenter, | |||||
| ), | |||||
| ), | |||||
| child: ListView.builder( | |||||
| reverse: true, | |||||
| itemCount: _logs.length, | |||||
| itemBuilder: (context, index) { | |||||
| final log = _logs[index]; | |||||
| return Text(log, style: TextStyle( | |||||
| // Logique de couleur conditionnelle pour la lisibilité | |||||
| color: log.contains('❌') | |||||
| ? Theme.of(context).colorScheme.error // Rouge pour les erreurs | |||||
| : (log.contains('✅') || log.contains('📦') || log.contains('✨')) | |||||
| ? Colors.green[600] // Vert pour les succès | |||||
| : Theme.of(context) | |||||
| .colorScheme | |||||
| .onSurface | |||||
| .withOpacity(0.8), // Couleur par défaut | |||||
| fontSize: 12 | |||||
| ), | |||||
| ); | |||||
| }, | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| // lib/presentation/widgets/creation_flow_layout.dart | |||||
| import 'package:flutter/material.dart'; | |||||
| class CreationProgressIndicator extends StatelessWidget { | |||||
| final int currentStep; | |||||
| final int totalSteps; | |||||
| const CreationProgressIndicator({ super.key, | |||||
| required this.currentStep, | |||||
| this.totalSteps = 3, | |||||
| }); | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return Padding( | |||||
| padding: const EdgeInsets.symmetric(vertical: 16.0), | |||||
| // --- MODIFICATION 1 : On centre la Row principale --- | |||||
| child: Center( | |||||
| child: Row( | |||||
| // On s'assure que la Row ne prend que la place nécessaire | |||||
| mainAxisSize: MainAxisSize.min, | |||||
| children: List.generate(totalSteps, (index) { | |||||
| bool isActive = index == currentStep; | |||||
| bool isCompleted = index < currentStep; | |||||
| bool isLast = index == totalSteps - 1; | |||||
| // --- MODIFICATION 2 : Chaque segment a maintenant une largeur fixe --- | |||||
| return Row( | |||||
| children: [ | |||||
| // Le cercle de l'étape | |||||
| Container( | |||||
| height: 24, | |||||
| width: 24, | |||||
| decoration: BoxDecoration( | |||||
| shape: BoxShape.circle, | |||||
| color: isActive || isCompleted | |||||
| ? Theme.of(context).primaryColor | |||||
| : Theme.of(context).colorScheme.surfaceVariant, | |||||
| ), | |||||
| child: Center( | |||||
| child: isCompleted | |||||
| ? const Icon(Icons.check, color: Colors.white, size: 16) | |||||
| : Text( | |||||
| '${index + 1}', | |||||
| style: TextStyle( | |||||
| color: isActive | |||||
| ? Colors.white | |||||
| : Theme.of(context).colorScheme.onSurfaceVariant, | |||||
| fontWeight: FontWeight.bold, | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| // La ligne de connexion (sauf pour la dernière) | |||||
| if (!isLast) | |||||
| Container( | |||||
| // On donne une largeur fixe à la ligne de connexion | |||||
| width: 60, | |||||
| height: 2, | |||||
| margin: const EdgeInsets.symmetric(horizontal: 8.0), | |||||
| color: isCompleted | |||||
| ? Theme.of(context).primaryColor | |||||
| : Theme.of(context).colorScheme.surfaceVariant, | |||||
| ), | |||||
| ], | |||||
| ); | |||||
| }), | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } | |||||
| // Le reste du fichier CreationFlowLayout reste inchangé | |||||
| class CreationFlowLayout extends StatelessWidget { | |||||
| final int currentStep; | |||||
| final String title; | |||||
| final Widget child; | |||||
| const CreationFlowLayout({ | |||||
| super.key, | |||||
| required this.currentStep, | |||||
| required this.title, | |||||
| required this.child, | |||||
| }); | |||||
| @override | |||||
| Widget build(BuildContext context) { | |||||
| return Scaffold( | |||||
| appBar: AppBar( | |||||
| title: Text(title), | |||||
| elevation: 0, | |||||
| centerTitle: true, | |||||
| ), | |||||
| body: Column( | |||||
| children: [ | |||||
| CreationProgressIndicator(currentStep: currentStep), | |||||
| const Divider(height: 1), | |||||
| Expanded( | |||||
| child: child, | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } |
| // lib/repositories/ai_repository.dart | |||||
| // 1. IMPORTER LES INTERFACES ET LES IMPLÉMENTATIONS NÉCESSAIRES | |||||
| // Le fichier ollama_service.dart contient nos nouvelles classes découpées. | |||||
| import '../services/ollama_service.dart'; | |||||
| import '../services/stable_diffusion_service.dart'; | |||||
| import '../services/gemini_service.dart'; | |||||
| import '../services/image_editing_service.dart'; | |||||
| // Énumération pour choisir le modèle de génération d'image. | |||||
| enum ImageGenerationModel { stableDiffusion, gemini } | |||||
| /// Le AiRepository est le point d'entrée centralisé pour toutes les opérations d'IA. | |||||
| /// L'interface utilisateur ne parlera qu'à ce Repository. | |||||
| class AiRepository { | |||||
| // --- NOS SERVICES SPÉCIALISÉS --- | |||||
| // Pour l'analyse initiale de l'image ("prompt 1"). | |||||
| // On utilise l'implémentation Ollama, mais on ne dépend que de l'interface. | |||||
| final ImageAnalysisService _imageAnalyzer = OllamaImageAnalysisService(); | |||||
| // Pour générer les idées de posts. | |||||
| final PostCreationService _postCreator = OllamaPostCreationService(); | |||||
| // Pour améliorer le texte. | |||||
| final TextImprovementService _textImprover = OllamaTextImprovementService(); | |||||
| // Une collection de services pour la GÉNÉRATION D'IMAGES. | |||||
| final Map<ImageGenerationModel, ImageEditingService> _imageGenerators; | |||||
| // Le constructeur initialise la collection de générateurs d'images. | |||||
| AiRepository() | |||||
| : _imageGenerators = { | |||||
| ImageGenerationModel.stableDiffusion: StableDiffusionService(), | |||||
| ImageGenerationModel.gemini: GeminiService(), | |||||
| }; | |||||
| // --- LES MÉTHODES PUBLIQUES UTILISÉES PAR L'INTERFACE UTILISATEUR --- | |||||
| /// Étape: `MediaPickerScreen` -> `AiEnhancementScreen` | |||||
| /// Analyse l'image pour obtenir une description textuelle (le "prompt 1"). | |||||
| /// C'est la tâche qui est lancée "en amont". | |||||
| Future<String> analyzeImageForPrompt(String base64Image) { | |||||
| print("[AiRepository] Délégation de l'analyse de l'image à l'ImageAnalyzer..."); | |||||
| return _imageAnalyzer.analyzeImage(base64Image); | |||||
| } | |||||
| /// Étape: `AiEnhancementScreen` | |||||
| /// Génère de nouvelles versions d'une image en utilisant un modèle spécifique. | |||||
| Stream<String> generateImageVariations({ | |||||
| required ImageGenerationModel model, | |||||
| required String base64Image, | |||||
| required String prompt, | |||||
| required int width, | |||||
| required int height, | |||||
| int numberOfImages = 1, | |||||
| }) { | |||||
| print("[AiRepository] Délégation de la génération d'images au modèle : $model"); | |||||
| final generator = _imageGenerators[model]; | |||||
| if (generator == null) { | |||||
| // Retourne un stream avec une erreur si le modèle n'est pas configuré. | |||||
| return Stream.error(Exception("Le modèle de génération d'image '$model' n'est pas disponible.")); | |||||
| } | |||||
| return generator.editImage(base64Image, prompt, width, height, numberOfImages: numberOfImages); | |||||
| } | |||||
| /// Étape: `TextGenerationScreen` | |||||
| /// Génère des idées de posts basées sur l'image et un profil utilisateur. | |||||
| Future<List<String>> generatePostIdeas({ | |||||
| required String base64Image, | |||||
| required String profession, | |||||
| required String tone, | |||||
| }) { | |||||
| print("[AiRepository] Délégation de la création de posts au PostCreator..."); | |||||
| return _postCreator.generatePostIdeas( | |||||
| base64Image: base64Image, | |||||
| profession: profession, | |||||
| tone: tone, | |||||
| ); | |||||
| } | |||||
| /// Étape: `PostRefinementScreen` | |||||
| /// Améliore un texte existant selon une instruction. | |||||
| Future<String> improvePostText({ | |||||
| required String originalText, | |||||
| required String userInstruction, | |||||
| }) { | |||||
| print("[AiRepository] Délégation de l'amélioration du texte au TextImprover..."); | |||||
| return _textImprover.improveText( | |||||
| originalText: originalText, | |||||
| userInstruction: userInstruction, | |||||
| ); | |||||
| } | |||||
| } |
| // lib/routes/app_routes.dart | |||||
| // Il n'est plus nécessaire d'importer les écrans ici | |||||
| /// Contient les noms des routes de l'application sous forme de constantes statiques. | |||||
| abstract final class AppRoutes { | |||||
| static const String profileSetup = '/profile-setup'; | |||||
| static const String mediaPicker = '/media-picker'; | |||||
| static const String aiEnhancement = '/ai-enhancement'; | |||||
| static const String textGeneration = '/text-generation'; | |||||
| static const String postPreview = '/post-preview'; | |||||
| static const String postRefinement = '/post-refinement'; // <-- Route conservée | |||||
| static const String export = '/export'; | |||||
| static const String imagePreview = '/image-preview'; | |||||
| static const String login = '/login'; | |||||
| static const String home = '/home'; | |||||
| // Ce bloc n'est plus utilisé car on utilise onGenerateRoute dans main.dart | |||||
| // Vous pouvez le supprimer complètement pour nettoyer le code. | |||||
| /* | |||||
| static Map<String, WidgetBuilder> get routes => { | |||||
| mediaPicker: (_) => const MediaPickerScreen(), | |||||
| profileSetup: (_) => const ProfileSetupScreen(), | |||||
| aiEnhancement: (_) => const AiEnhancementScreen(), | |||||
| textGeneration: (_) => const TextGenerationScreen(), | |||||
| postPreview: (_) => const PostPreviewScreen(), | |||||
| export: (_) => const ExportScreen(), | |||||
| imagePreview: (_) => const ImagePreviewScreen(), | |||||
| }; | |||||
| */ | |||||
| } |
| // lib/services/gemini_service.dart | |||||
| import 'dart:async';import 'dart:convert'; | |||||
| import 'package:http/http.dart' as http; | |||||
| import 'image_editing_service.dart'; | |||||
| class GeminiService implements ImageEditingService { | |||||
| final String _apiKey; | |||||
| // L'URL que vous utilisiez et qui fonctionnait | |||||
| final String _apiUrl = | |||||
| 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent'; | |||||
| GeminiService({String? apiKey}) : _apiKey = apiKey ?? 'AIzaSyBQpeyl-Qi8QoCfwmJoxAumYYI5-nwit4Q'; // Remplacez par votre clé | |||||
| /// EditImage qui retourne un Stream, comme attendu par l'interface. | |||||
| @override | |||||
| Stream<String> editImage(String base64Image, | |||||
| String prompt, | |||||
| int width, | |||||
| int height, | |||||
| {int numberOfImages = 1}) { | |||||
| // 1. On crée un StreamController | |||||
| final controller = StreamController<String>(); | |||||
| // 2. On lance la génération en arrière-plan | |||||
| _generateAndStreamImage(controller, base64Image, prompt, width, height); | |||||
| // 3. On retourne le stream immédiatement | |||||
| return controller.stream; | |||||
| } | |||||
| /// Méthode privée qui contient la logique de génération correcte. | |||||
| Future<void> _generateAndStreamImage(StreamController<String> controller, | |||||
| String base64Image, | |||||
| String prompt, | |||||
| int width, | |||||
| int height) async { | |||||
| print("[GeminiService] 🚀 Lancement de la génération d'image..."); | |||||
| final editPrompt = _buildEditPrompt(prompt, width, height); | |||||
| final requestBody = { | |||||
| 'contents': [ | |||||
| { | |||||
| 'parts': [ | |||||
| {'text': editPrompt}, | |||||
| { | |||||
| 'inlineData': { | |||||
| 'mimeType': 'image/jpeg', | |||||
| 'data': base64Image, | |||||
| } | |||||
| } | |||||
| ] | |||||
| } | |||||
| ], | |||||
| 'generationConfig': { | |||||
| // La documentation la plus récente suggère d'utiliser 'tool_config' pour ce genre de tâche | |||||
| // mais nous allons garder la version qui marchait pour vous. | |||||
| // Si une erreur survient, ce sera le premier endroit à vérifier. | |||||
| // 👇👇👇 CETTE LIGNE EST LA CLÉ DU SUCCÈS 👇👇👇 | |||||
| 'responseModalities': ['IMAGE', 'TEXT'], | |||||
| } | |||||
| }; | |||||
| try { | |||||
| // --- CORRECTION DE L'URL --- | |||||
| // L'action ':generateContent' est déjà dans la variable _apiUrl. | |||||
| final response = await http | |||||
| .post( | |||||
| Uri.parse('$_apiUrl?key=$_apiKey'), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ) | |||||
| .timeout(const Duration(minutes: 5)); | |||||
| if (response.statusCode == 200) { | |||||
| final responseData = jsonDecode(response.body); | |||||
| final String singleImage = _extractImageFromResponse(responseData); | |||||
| print("[GeminiService] ✅ Image reçue. Envoi dans le stream..."); | |||||
| controller.add(singleImage); | |||||
| } else { | |||||
| final errorBody = jsonDecode(response.body); | |||||
| final errorMessage = errorBody['error']?['message'] ?? response.body; | |||||
| throw Exception('Erreur Gemini ${response.statusCode}: $errorMessage'); | |||||
| } | |||||
| } catch (e, s) { | |||||
| print("[GeminiService] ❌ Erreur lors de la génération: $e"); | |||||
| controller.addError(e, s); | |||||
| } finally { | |||||
| print("[GeminiService] Stream fermé."); | |||||
| controller.close(); | |||||
| } | |||||
| } | |||||
| // --- TOUTES LES MÉTHODES HELPER SONT MAINTENANT À L'INTÉRIEUR DE LA CLASSE --- | |||||
| String _buildEditPrompt(String userPrompt, int width, int height) { | |||||
| return '''You are a professional photo editor. Based on the provided image and instructions below, | |||||
| generate an edited version that enhances the photo while maintaining naturalness and authenticity. | |||||
| INSTRUCTIONS: | |||||
| $userPrompt | |||||
| EDITING GUIDELINES: | |||||
| - Preserve the original composition and subject | |||||
| - Maintain realistic skin textures (no plastic look) | |||||
| - Enhance lighting naturally with warm, soft tones | |||||
| - Improve colors while keeping them true-to-life | |||||
| - Add subtle details and clarity without over-processing | |||||
| - Keep the image aspect ratio and dimensions | |||||
| - Ensure the final result looks professional and Instagram-ready | |||||
| CONSTRAINTS: | |||||
| - Do NOT add watermarks or text | |||||
| - Do NOT change the main subject dramatically | |||||
| - Do NOT apply artificial filters or effects | |||||
| - Keep editing subtle and professional | |||||
| Generate the edited image following these guidelines.'''; | |||||
| } | |||||
| // --- MÉTHODE D'EXTRACTION CORRIGÉE --- | |||||
| String _extractImageFromResponse(Map<String, dynamic> response) { | |||||
| try { | |||||
| final candidates = response['candidates'] as List<dynamic>?; | |||||
| if (candidates == null || candidates.isEmpty) { | |||||
| // Si il n'y a aucun candidat, c'est que le prompt a été bloqué AVANT la génération. | |||||
| final promptFeedback = response['promptFeedback']; | |||||
| if (promptFeedback != null && promptFeedback['blockReason'] != null) { | |||||
| throw Exception("Le prompt a été bloqué par Gemini pour la raison : ${promptFeedback['blockReason']}."); | |||||
| } | |||||
| throw Exception('Réponse invalide de Gemini : aucun "candidate" trouvé.'); | |||||
| } | |||||
| final candidate = candidates.first; | |||||
| // 1. Vérifier la raison de la fin AVANT de chercher le contenu. | |||||
| final finishReason = candidate['finishReason'] as String?; | |||||
| if (finishReason != null && finishReason != 'STOP') { | |||||
| if (finishReason == 'SAFETY') { | |||||
| throw Exception("La génération a été stoppée par Gemini pour des raisons de sécurité (SAFETY). L'image ou le prompt a été jugé inapproprié."); | |||||
| } | |||||
| throw Exception("La génération s'est terminée prématurément. Raison : $finishReason"); | |||||
| } | |||||
| // 2. Si tout va bien, on peut maintenant chercher le contenu. | |||||
| final content = candidate['content'] as Map<String, dynamic>?; | |||||
| if (content == null) { | |||||
| throw Exception('Pas de "content" dans la première candidate, malgré un "finishReason" correct. Réponse inattendue.'); | |||||
| } | |||||
| final parts = content['parts'] as List<dynamic>?; | |||||
| if (parts == null || parts.isEmpty) { | |||||
| throw Exception('Pas de "parts" dans le content'); | |||||
| } | |||||
| for (final part in parts) { | |||||
| final inlineData = part['inlineData'] as Map<String, dynamic>?; | |||||
| if (inlineData != null && inlineData.containsKey('data')) { | |||||
| return inlineData['data'] as String; | |||||
| } | |||||
| } | |||||
| throw Exception("Pas d'image (inlineData) dans les parts de la réponse."); | |||||
| } catch (e) { | |||||
| // Afficher la réponse brute aide toujours à déboguer | |||||
| print("--- Réponse brute de Gemini lors de l'erreur d'extraction ---"); | |||||
| print(jsonEncode(response)); | |||||
| print("------------------------------------------------------------"); | |||||
| // Renvoie l'erreur spécifique que nous avons construite. | |||||
| throw Exception('Erreur d\'extraction de l\'image: $e'); | |||||
| } | |||||
| } | |||||
| @override | |||||
| Future<String> generatePrompt(String base64Image) async { | |||||
| print("[GeminiService] 🚀 Lancement de l'analyse d'image pour le prompt..."); | |||||
| final requestBody = { | |||||
| 'contents': [ | |||||
| { | |||||
| 'parts': [ | |||||
| { | |||||
| 'text': | |||||
| 'Analyze this image and suggest specific professional photo enhancements and a good instagram publication text. Focus on: lighting, colors, composition, and overall aesthetic. Be concise and actionable.' | |||||
| }, | |||||
| { | |||||
| 'inlineData': { | |||||
| 'mimeType': 'image/jpeg', | |||||
| 'data': base64Image, | |||||
| } | |||||
| } | |||||
| ] | |||||
| } | |||||
| ] | |||||
| }; | |||||
| try { | |||||
| final response = await http | |||||
| .post( | |||||
| Uri.parse('$_apiUrl?key=$_apiKey'), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ) | |||||
| .timeout(const Duration(minutes: 3)); | |||||
| if (response.statusCode == 200) { | |||||
| final responseData = jsonDecode(response.body); | |||||
| return _extractTextFromResponse(responseData); | |||||
| } else { | |||||
| throw Exception('Erreur Gemini ${response.statusCode}: ${response.body}'); | |||||
| } | |||||
| } catch (e) { | |||||
| throw Exception('Erreur génération prompt Gemini: $e'); | |||||
| } | |||||
| } | |||||
| String _extractTextFromResponse(Map<String, dynamic> response) { | |||||
| try { | |||||
| final candidates = response['candidates'] as List<dynamic>?; | |||||
| if (candidates == null || candidates.isEmpty) { | |||||
| return ''; | |||||
| } | |||||
| final content = candidates[0]['content'] as Map<String, dynamic>?; | |||||
| if (content == null) { | |||||
| return ''; | |||||
| } | |||||
| final parts = content['parts'] as List<dynamic>?; | |||||
| if (parts == null || parts.isEmpty) { | |||||
| return ''; | |||||
| } | |||||
| final buffer = StringBuffer(); | |||||
| for (final part in parts) { | |||||
| final text = part['text'] as String?; | |||||
| if (text != null && text.isNotEmpty) { | |||||
| buffer.writeln(text.trim()); | |||||
| } | |||||
| } | |||||
| return buffer.toString().trim(); | |||||
| } catch (e) { | |||||
| print('Erreur extraction texte: $e'); | |||||
| return ''; | |||||
| } | |||||
| } | |||||
| } |
| import 'dart:convert'; | |||||
| import 'package:http/http.dart' as http; | |||||
| /// Définit un contrat pour tout service capable d'analyser une image | |||||
| /// et de la décrire sous forme de texte. | |||||
| abstract class ImageAnalysisService { | |||||
| /// Prend une image en base64 et retourne une description textuelle (prompt). | |||||
| Future<String> analyzeImage(String base64Image); | |||||
| } | |||||
| /// L'implémentation Ollama de ce service. | |||||
| class OllamaImageAnalysisService implements ImageAnalysisService { | |||||
| final String _apiUrl = 'http://192.168.20.200:11434/api/generate'; | |||||
| final String _visionModel = 'llava:7b'; | |||||
| @override | |||||
| Future<String> analyzeImage(String base64Image) async { | |||||
| print( | |||||
| "[OllamaImageAnalysisService] 🚀 Lancement de l'analyse de l'image..."); | |||||
| final requestPrompt = ''' | |||||
| As a professional photographe and instagram, describe this imange and give ideas to improve quality to publish on social network. | |||||
| '''; | |||||
| final requestBody = { | |||||
| 'model': _visionModel, | |||||
| 'prompt': requestPrompt, | |||||
| 'images': [base64Image], | |||||
| 'stream': false, | |||||
| }; | |||||
| try { | |||||
| final response = await http.post( | |||||
| Uri.parse(_apiUrl), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ).timeout(const Duration(minutes: 2)); | |||||
| if (response.statusCode == 200) { | |||||
| final body = jsonDecode(response.body); | |||||
| final generatedPrompt = (body['response'] as String? ?? '') | |||||
| .trim() | |||||
| .replaceAll('\n', ' '); | |||||
| print( | |||||
| "[OllamaImageAnalysisService] ✅ Analyse terminée : $generatedPrompt"); | |||||
| return generatedPrompt; | |||||
| } else { | |||||
| throw Exception( | |||||
| 'Erreur Ollama (analyzeImage) ${response.statusCode}: ${response | |||||
| .body}'); | |||||
| } | |||||
| } catch (e) { | |||||
| print("[OllamaImageAnalysisService] ❌ Exception : ${e.toString()}"); | |||||
| rethrow; | |||||
| } | |||||
| } | |||||
| } |
| // lib/services/image_editing_service.dart | |||||
| abstract class ImageEditingService { | |||||
| /// Génère un prompt descriptif à partir d'une image. | |||||
| Future<String> generatePrompt(String base64Image); | |||||
| /// Modifie une image en se basant sur un prompt. | |||||
| Stream<String> editImage(String base64Image, String prompt, int width, int height, {int numberOfImages = 1}); | |||||
| } |
| // lib/services/ollama_service.dart | |||||
| import 'dart:convert'; | |||||
| import 'package:http/http.dart' as http; | |||||
| // ========================================================================= | |||||
| // SERVICE 1: ANALYSE D'IMAGE (POUR LE PROMPT INITIAL) | |||||
| // ========================================================================= | |||||
| /// Définit un contrat pour tout service capable d'analyser une image | |||||
| /// et de la décrire sous forme de texte. | |||||
| abstract class ImageAnalysisService { | |||||
| /// Prend une image en base64 et retourne une description textuelle (prompt). | |||||
| Future<String> analyzeImage(String base64Image); | |||||
| } | |||||
| /// L'implémentation Ollama de ce service. | |||||
| class OllamaImageAnalysisService implements ImageAnalysisService { | |||||
| final String _apiUrl = 'http://192.168.20.200:11434/api/generate'; | |||||
| final String _visionModel = 'llava:7b'; | |||||
| @override | |||||
| Future<String> analyzeImage(String base64Image) async { | |||||
| print("[OllamaImageAnalysisService] 🚀 Lancement de l'analyse de l'image..."); | |||||
| final requestPrompt = ''' | |||||
| Décris cette image en une phrase courte et factuelle, comme un prompt pour une IA. | |||||
| Concentre-toi sur le sujet principal, son action et l'environnement. | |||||
| Sois direct, concis et ne mentionne pas le mot 'image' ou 'photo'. | |||||
| '''; | |||||
| final requestBody = { | |||||
| 'model': _visionModel, | |||||
| 'prompt': requestPrompt, | |||||
| 'images': [base64Image], | |||||
| 'stream': false, | |||||
| }; | |||||
| try { | |||||
| final response = await http.post( | |||||
| Uri.parse(_apiUrl), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ).timeout(const Duration(minutes: 2)); | |||||
| if (response.statusCode == 200) { | |||||
| final body = jsonDecode(response.body); | |||||
| final generatedPrompt = (body['response'] as String? ?? '').trim().replaceAll('\n', ' '); | |||||
| print("[OllamaImageAnalysisService] ✅ Analyse terminée : $generatedPrompt"); | |||||
| return generatedPrompt; | |||||
| } else { | |||||
| throw Exception('Erreur Ollama (analyzeImage) ${response.statusCode}: ${response.body}'); | |||||
| } | |||||
| } catch (e) { | |||||
| print("[OllamaImageAnalysisService] ❌ Exception : ${e.toString()}"); | |||||
| rethrow; | |||||
| } | |||||
| } | |||||
| } | |||||
| // ========================================================================= | |||||
| // SERVICE 2: CRÉATION DE POSTS SOCIAUX | |||||
| // ========================================================================= | |||||
| /// Définit un contrat pour tout service capable de générer des idées de posts. | |||||
| abstract class PostCreationService { | |||||
| Future<List<String>> generatePostIdeas({ | |||||
| required String base64Image, | |||||
| required String profession, | |||||
| required String tone, | |||||
| }); | |||||
| } | |||||
| /// L'implémentation Ollama de ce service. | |||||
| class OllamaPostCreationService implements PostCreationService { | |||||
| final String _apiUrl = 'http://192.168.20.200:11434/api/generate'; | |||||
| final String _visionModel = 'llava:7b'; | |||||
| @override | |||||
| Future<List<String>> generatePostIdeas({ | |||||
| required String base64Image, | |||||
| required String profession, | |||||
| required String tone, | |||||
| }) async { | |||||
| final requestPrompt = """ | |||||
| You are a social media expert. | |||||
| Act as a "$profession". | |||||
| Analyze the image. | |||||
| Generate 3 short and engaging social media post ideas in french with a "$tone" tone. | |||||
| Your output MUST be a valid JSON array of strings. | |||||
| Example: | |||||
| ["Idée de post 1...", "Idée de post 2...", "Idée de post 3..."] | |||||
| """; | |||||
| final requestBody = { | |||||
| 'model': _visionModel, | |||||
| 'prompt': requestPrompt, | |||||
| 'images': [base64Image], | |||||
| 'stream': false, | |||||
| }; | |||||
| try { | |||||
| print("[OllamaPostCreationService] 🚀 Appel pour générer des idées..."); | |||||
| final response = await http.post( | |||||
| Uri.parse(_apiUrl), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ).timeout(const Duration(minutes: 3)); | |||||
| if (response.statusCode == 200) { | |||||
| final responseData = jsonDecode(response.body); | |||||
| final jsonString = (responseData['response'] as String? ?? '').trim(); | |||||
| if (jsonString.isEmpty) return []; | |||||
| try { | |||||
| final ideasList = jsonDecode(jsonString) as List; | |||||
| return ideasList.map((idea) => idea.toString()).toList(); | |||||
| } catch (e) { | |||||
| print("[OllamaPostCreationService] ❌ Erreur de parsing JSON. Réponse : $jsonString"); | |||||
| return [jsonString]; // Retourne la réponse brute comme une seule idée | |||||
| } | |||||
| } else { | |||||
| throw Exception('Erreur Ollama (generatePostIdeas) ${response.statusCode}: ${response.body}'); | |||||
| } | |||||
| } catch (e) { | |||||
| print("[OllamaPostCreationService] ❌ Exception : ${e.toString()}"); | |||||
| rethrow; | |||||
| } | |||||
| } | |||||
| } | |||||
| // ========================================================================= | |||||
| // SERVICE 3: AMÉLIORATION DE TEXTE | |||||
| // ========================================================================= | |||||
| /// Définit un contrat pour tout service capable d'améliorer un texte. | |||||
| abstract class TextImprovementService { | |||||
| Future<String> improveText({ | |||||
| required String originalText, | |||||
| required String userInstruction, | |||||
| }); | |||||
| } | |||||
| /// L'implémentation Ollama de ce service. | |||||
| class OllamaTextImprovementService implements TextImprovementService { | |||||
| final String _apiUrl = 'http://192.168.20.200:11434/api/generate'; | |||||
| final String _textModel = 'gpt-oss:20b'; | |||||
| @override | |||||
| Future<String> improveText({ | |||||
| required String originalText, | |||||
| required String userInstruction, | |||||
| }) async { | |||||
| print("[OllamaTextImprovementService] 🚀 Appel pour améliorer le texte..."); | |||||
| final requestPrompt = """ | |||||
| You are a social media writing assistant. | |||||
| A user wants to improve the following text: | |||||
| --- TEXT TO IMPROVE --- | |||||
| $originalText | |||||
| ----------------------- | |||||
| The user's instruction is: "$userInstruction". | |||||
| Rewrite the text based on the instruction. | |||||
| Your output MUST be ONLY the improved text, without any extra commentary or explanations. | |||||
| """; | |||||
| final requestBody = { | |||||
| 'model': _textModel, | |||||
| 'prompt': requestPrompt, | |||||
| 'stream': false, | |||||
| }; | |||||
| try { | |||||
| final response = await http.post( | |||||
| Uri.parse(_apiUrl), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ).timeout(const Duration(minutes: 2)); | |||||
| if (response.statusCode == 200) { | |||||
| final responseData = jsonDecode(response.body); | |||||
| return (responseData['response'] as String? ?? '').trim(); | |||||
| } else { | |||||
| throw Exception('Erreur Ollama (improveText) ${response.statusCode}: ${response.body}'); | |||||
| } | |||||
| } catch (e) { | |||||
| print("[OllamaTextImprovementService] ❌ Exception : ${e.toString()}"); | |||||
| rethrow; | |||||
| } | |||||
| } | |||||
| } | |||||
| import 'dart:convert'; | |||||
| import 'package:http/http.dart' as http; | |||||
| /// Définit un contrat pour tout service capable de générer des idées de posts. | |||||
| abstract class PostCreationService { | |||||
| Future<List<String>> generatePostIdeas({ | |||||
| required String base64Image, | |||||
| required String profession, | |||||
| required String tone, | |||||
| }); | |||||
| } | |||||
| /// L'implémentation Ollama de ce service. | |||||
| class OllamaPostCreationService implements PostCreationService { | |||||
| final String _apiUrl = 'http://192.168.20.200:11434/api/generate'; | |||||
| final String _visionModel = 'llava:7b'; | |||||
| @override | |||||
| Future<List<String>> generatePostIdeas({ | |||||
| required String base64Image, | |||||
| required String profession, | |||||
| required String tone, | |||||
| }) async { | |||||
| final requestPrompt = """ | |||||
| You are a social media expert. | |||||
| Act as a "$profession". | |||||
| Analyze the image. | |||||
| Generate 3 short and engaging social media post ideas in french with a "$tone" tone. | |||||
| Your output MUST be a valid JSON array of strings. | |||||
| Example: | |||||
| ["", "", ""] | |||||
| """; | |||||
| final requestBody = { | |||||
| 'model': _visionModel, | |||||
| 'prompt': requestPrompt, | |||||
| 'images': [base64Image], | |||||
| 'stream': false, | |||||
| }; | |||||
| try { | |||||
| print("[OllamaPostCreationService] 🚀 Appel pour générer des idées..."); | |||||
| final response = await http.post( | |||||
| Uri.parse(_apiUrl), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ).timeout(const Duration(minutes: 3)); | |||||
| if (response.statusCode == 200) { | |||||
| final responseData = jsonDecode(response.body); | |||||
| final jsonString = (responseData['response'] as String? ?? '').trim(); | |||||
| if (jsonString.isEmpty) return []; | |||||
| try { | |||||
| final ideasList = jsonDecode(jsonString) as List; | |||||
| return ideasList.map((idea) => idea.toString()).toList(); | |||||
| } catch (e) { | |||||
| print("[OllamaPostCreationService] ❌ Erreur de parsing JSON. Réponse : $jsonString"); | |||||
| return [jsonString]; // Retourne la réponse brute comme une seule idée | |||||
| } | |||||
| } else { | |||||
| throw Exception('Erreur Ollama (generatePostIdeas) ${response.statusCode}: ${response.body}'); | |||||
| } | |||||
| } catch (e) { | |||||
| print("[OllamaPostCreationService] ❌ Exception : ${e.toString()}"); | |||||
| rethrow; | |||||
| } | |||||
| } | |||||
| } |
| // lib/services/stable_diffusion_service.dart | |||||
| import 'dart:async'; | |||||
| import 'dart:convert'; | |||||
| import 'dart:typed_data'; | |||||
| import 'package:flutter/foundation.dart'; | |||||
| import 'package:http/http.dart' as http; | |||||
| import 'package:image/image.dart' as img; | |||||
| import 'image_editing_service.dart'; | |||||
| // --- Définitions des filtres (isolés pour être utilisés avec compute) --- | |||||
| // Look-up table pour les couleurs | |||||
| typedef ColorLut = ({List<int> r, List<int> g, List<int> b}); | |||||
| // Filtre 1: Applique un filtre chaud via une LUT | |||||
| Uint8List _applyWarmthFilter((Uint8List, ColorLut) params) { | |||||
| final imageBytes = params.$1; | |||||
| final lut = params.$2; | |||||
| final image = img.decodeImage(imageBytes); | |||||
| if (image == null) return imageBytes; | |||||
| for (final pixel in image) { | |||||
| pixel.r = lut.r[pixel.r.toInt()]; | |||||
| pixel.g = lut.g[pixel.g.toInt()]; | |||||
| pixel.b = lut.b[pixel.b.toInt()]; | |||||
| } | |||||
| return Uint8List.fromList(img.encodeJpg(image, quality: 95)); | |||||
| } | |||||
| // Filtre 2: Augmente le contraste et la saturation | |||||
| Uint8List _applyContrastSaturationFilter(Uint8List imageBytes) { | |||||
| final image = img.decodeImage(imageBytes); | |||||
| if (image == null) return imageBytes; | |||||
| img.adjustColor(image, contrast: 1.2, saturation: 1.15); | |||||
| return Uint8List.fromList(img.encodeJpg(image, quality: 95)); | |||||
| } | |||||
| // Calcule la LUT pour le filtre chaud | |||||
| ColorLut _computeWarmthLut() { | |||||
| final rLut = List.generate(256, (i) => (i * 1.15).clamp(0, 255).toInt()); | |||||
| final gLut = List.generate(256, (i) => (i * 1.05).clamp(0, 255).toInt()); | |||||
| final bLut = List.generate(256, (i) => (i * 0.90).clamp(0, 255).toInt()); | |||||
| return (r: rLut, g: gLut, b: bLut); | |||||
| } | |||||
| class StableDiffusionService implements ImageEditingService { | |||||
| final String _apiUrl = 'http://192.168.20.200:7860/sdapi/v1/img2img'; | |||||
| @override | |||||
| Future<String> generatePrompt(String base64Image) async { | |||||
| throw UnimplementedError('Stable Diffusion ne peut pas générer de prompt.'); | |||||
| } | |||||
| @override | |||||
| Stream<String> editImage(String base64Image, String prompt, int width, int height, {int numberOfImages = 3}) { | |||||
| final controller = StreamController<String>(); | |||||
| _generateVariations(controller, base64Image, prompt, width, height); | |||||
| return controller.stream; | |||||
| } | |||||
| /// NOUVELLE LOGIQUE DE GÉNÉRATION, PLUS EFFICACE | |||||
| Future<void> _generateVariations(StreamController<String> controller, String base64Image, String prompt, int width, int height) async { | |||||
| try { | |||||
| // 1. On fait UN SEUL appel à Stable Diffusion pour une variation de base. | |||||
| final response = await _createImageRequest( | |||||
| base64Image: base64Image, | |||||
| prompt: "$prompt, high quality, sharp focus", // Prompt générique de qualité | |||||
| width: width, | |||||
| height: height, | |||||
| denoisingStrength: 0.30, // Assez pour une variation notable | |||||
| cfgScale: 7.0, | |||||
| ); | |||||
| if (response.statusCode != 200) { | |||||
| throw Exception('Erreur Stable Diffusion ${response.statusCode}: ${response.body}'); | |||||
| } | |||||
| final responseData = jsonDecode(response.body); | |||||
| final generatedImageBase64 = (responseData['images'] as List).first as String; | |||||
| // La variation de base est notre première image. | |||||
| controller.add(generatedImageBase64); | |||||
| // 2. On prépare les filtres à appliquer sur cette variation de base. | |||||
| final generatedImageBytes = base64Decode(generatedImageBase64); | |||||
| final warmthLut = _computeWarmthLut(); | |||||
| // 3. On lance les deux filtres en parallèle pour plus de rapidité. | |||||
| final futureWarm = compute(_applyWarmthFilter, (generatedImageBytes, warmthLut)); | |||||
| final futureContrast = compute(_applyContrastSaturationFilter, generatedImageBytes); | |||||
| // 4. On attend les résultats des filtres et on les ajoute au stream. | |||||
| final results = await Future.wait([futureWarm, futureContrast]); | |||||
| for (final filteredBytes in results) { | |||||
| controller.add(base64Encode(filteredBytes)); | |||||
| } | |||||
| } catch (e, stackTrace) { | |||||
| controller.addError(e, stackTrace); | |||||
| } finally { | |||||
| controller.close(); // On ferme le stream quand tout est fini. | |||||
| } | |||||
| } | |||||
| Future<http.Response> _createImageRequest({ | |||||
| required String base64Image, | |||||
| required String prompt, | |||||
| required int width, | |||||
| required int height, | |||||
| required double denoisingStrength, | |||||
| required double cfgScale, | |||||
| }) { | |||||
| final requestBody = { | |||||
| 'init_images': [base64Image], | |||||
| 'prompt': prompt, | |||||
| 'seed': -1, | |||||
| 'negative_prompt': 'blurry, low quality, artifacts, distorted, oversaturated, plastic skin, over-sharpen, artificial, harsh shadows, filters, watermark, cold lighting, blue tones, washed out, unnatural, sepia, text, letters', | |||||
| 'steps': 25, // 25 est souvent suffisant pour img2img | |||||
| 'cfg_scale': cfgScale, | |||||
| 'denoising_strength': denoisingStrength, | |||||
| 'sampler_name': 'DPM++ 2M Karras', | |||||
| 'width': width, | |||||
| 'height': height, | |||||
| 'restore_faces': false, | |||||
| }; | |||||
| return http.post( | |||||
| Uri.parse(_apiUrl), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ).timeout(const Duration(minutes: 5)); | |||||
| } | |||||
| } |
| import 'dart:convert'; | |||||
| import 'package:http/http.dart' as http; | |||||
| /// Définit un contrat pour tout service capable d'améliorer un texte. | |||||
| abstract class TextImprovementService { | |||||
| Future<String> improveText({ | |||||
| required String originalText, | |||||
| required String userInstruction, | |||||
| }); | |||||
| } | |||||
| /// L'implémentation Ollama de ce service. | |||||
| class OllamaTextImprovementService implements TextImprovementService { | |||||
| final String _apiUrl = 'http://192.168.20.200:11434/api/generate'; | |||||
| final String _textModel = 'gpt-oss:20b'; | |||||
| @override | |||||
| Future<String> improveText({ | |||||
| required String originalText, | |||||
| required String userInstruction, | |||||
| }) async { | |||||
| print("[OllamaTextImprovementService] 🚀 Appel pour améliorer le texte..."); | |||||
| final requestPrompt = """ | |||||
| You are a social media writing assistant. | |||||
| A user wants to improve the following text: | |||||
| --- TEXT TO IMPROVE --- | |||||
| $originalText | |||||
| ----------------------- | |||||
| The user's instruction is: "$userInstruction". | |||||
| Rewrite the text based on the instruction. | |||||
| Your output MUST be ONLY the improved text, without any extra commentary or explanations. | |||||
| """; | |||||
| final requestBody = { | |||||
| 'model': _textModel, | |||||
| 'prompt': requestPrompt, | |||||
| 'stream': false, | |||||
| }; | |||||
| try { | |||||
| final response = await http.post( | |||||
| Uri.parse(_apiUrl), | |||||
| headers: {'Content-Type': 'application/json'}, | |||||
| body: jsonEncode(requestBody), | |||||
| ).timeout(const Duration(minutes: 2)); | |||||
| if (response.statusCode == 200) { | |||||
| final responseData = jsonDecode(response.body); | |||||
| return (responseData['response'] as String? ?? '').trim(); | |||||
| } else { | |||||
| throw Exception('Erreur Ollama (improveText) ${response.statusCode}: ${response.body}'); | |||||
| } | |||||
| } catch (e) { | |||||
| print("[OllamaTextImprovementService] ❌ Exception : ${e.toString()}"); | |||||
| rethrow; | |||||
| } | |||||
| } | |||||
| } |
| # This file is automatically generated by Android Studio. | |||||
| # Do not modify this file -- YOUR CHANGES WILL BE ERASED! | |||||
| sdk.dir=/path/to/android/sdk | |||||
| flutter.sdk=/path/to/flutter/sdk | |||||
| flutter.buildMode=debug | |||||
| flutter.versionName=1.0.0 | |||||
| flutter.versionCode=1 |
| name: social_content_creator | |||||
| description: Application Flutter pour créer du contenu IA pour réseaux sociaux avec profils personnalisés. | |||||
| publish_to: 'none' | |||||
| version: 1.0.0+1 | |||||
| environment: | |||||
| sdk: '>=3.8.0 <4.0.0' | |||||
| flutter: '>=3.35.0' | |||||
| dependencies: | |||||
| flutter: | |||||
| sdk: flutter | |||||
| image: ^4.0.0 | |||||
| # UI & Design | |||||
| cupertino_icons: ^1.0.8 | |||||
| google_fonts: ^6.3.2 | |||||
| # Image & Media Handling | |||||
| image_picker: ^1.2.0 | |||||
| cached_network_image: ^3.4.0 | |||||
| # HTTP & API | |||||
| http: ^1.3.0 | |||||
| dio: ^5.6.0 | |||||
| # State Management | |||||
| provider: ^6.1.2 | |||||
| riverpod: ^2.6.0 | |||||
| # File Handling | |||||
| path_provider: ^2.1.3 | |||||
| archive: ^3.6.0 | |||||
| # Social Sharing | |||||
| share_plus: ^8.0.0 | |||||
| # Storage | |||||
| shared_preferences: ^2.3.0 | |||||
| # Utilities | |||||
| path: ^1.9.0 | |||||
| intl: ^0.20.0 | |||||
| uuid: ^4.4.0 | |||||
| flutter_facebook_auth: ^7.1.0 | |||||
| dev_dependencies: | |||||
| flutter_test: | |||||
| sdk: flutter | |||||
| flutter_lints: ^4.0.0 | |||||
| build_runner: ^2.4.0 | |||||
| flutter: | |||||
| uses-material-design: true | |||||
| assets: | |||||
| - assets/images/ | |||||
| - assets/icons/ | |||||
| fonts: | |||||
| - family: Inter | |||||
| fonts: | |||||
| - asset: assets/fonts/Inter-Regular.ttf | |||||
| - asset: assets/fonts/Inter-Bold.ttf | |||||
| weight: 700 | |||||
| - asset: assets/fonts/Inter-SemiBold.ttf | |||||
| weight: 600 |
| // This is a basic Flutter widget test. | |||||
| // | |||||
| // To perform an interaction with a widget in your test, use the WidgetTester | |||||
| // utility in the flutter_test package. For example, you can send tap and scroll | |||||
| // gestures. You can also use WidgetTester to find child widgets in the widget | |||||
| // tree, read text, and verify that the values of widget properties are correct. | |||||
| import 'package:flutter/material.dart'; | |||||
| import 'package:flutter_test/flutter_test.dart'; | |||||
| import 'package:social_content_creator/main.dart'; | |||||
| void main() { | |||||
| testWidgets('Counter increments smoke test', (WidgetTester tester) async { | |||||
| // Build our app and trigger a frame. | |||||
| await tester.pumpWidget(const MyApp()); | |||||
| // Verify that our counter starts at 0. | |||||
| expect(find.text('0'), findsOneWidget); | |||||
| expect(find.text('1'), findsNothing); | |||||
| // Tap the '+' icon and trigger a frame. | |||||
| await tester.tap(find.byIcon(Icons.add)); | |||||
| await tester.pump(); | |||||
| // Verify that our counter has incremented. | |||||
| expect(find.text('0'), findsNothing); | |||||
| expect(find.text('1'), findsOneWidget); | |||||
| }); | |||||
| } |