Переглянути джерело

initial commit GEMINI OK AND OLLAMAOK

master
Yann Pugliese 4 тижднів тому
коміт
0d5129160f
73 змінених файлів з 5313 додано та 0 видалено
  1. +78
    -0
      .gitignore
  2. +30
    -0
      .metadata
  3. +116
    -0
      README.md
  4. +170
    -0
      analysis_options.yaml
  5. +14
    -0
      android/.gitignore
  6. +54
    -0
      android/app/build.gradle.kts
  7. +14
    -0
      android/app/proguard-rules.pro
  8. +7
    -0
      android/app/src/debug/AndroidManifest.xml
  9. +53
    -0
      android/app/src/main/AndroidManifest.xml
  10. +12
    -0
      android/app/src/main/kotlin/com/example/social_content_creator/MainActivity.kt
  11. +12
    -0
      android/app/src/main/res/drawable-v21/launch_background.xml
  12. +12
    -0
      android/app/src/main/res/drawable/launch_background.xml
  13. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  14. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  15. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  16. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  17. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  18. +18
    -0
      android/app/src/main/res/values-night/styles.xml
  19. +18
    -0
      android/app/src/main/res/values/styles.xml
  20. +6
    -0
      android/app/src/main/res/xml/file_paths.xml
  21. +7
    -0
      android/app/src/profile/AndroidManifest.xml
  22. +20
    -0
      android/build.gradle.kts
  23. +8
    -0
      android/gradle.properties
  24. +5
    -0
      android/gradle/wrapper/gradle-wrapper.properties
  25. BIN
      android/java_pid15420.hprof
  26. +37
    -0
      android/settings.gradle.kts
  27. +0
    -0
      assets/fonts/.gitkeep
  28. BIN
      assets/fonts/Inter-Bold.ttf
  29. BIN
      assets/fonts/Inter-Regular.ttf
  30. BIN
      assets/fonts/Inter-SemiBold.ttf
  31. +0
    -0
      assets/icons/.gitkeep
  32. +0
    -0
      assets/images/.gitkeep
  33. BIN
      assets/images/placeholder.jpg
  34. +3
    -0
      devtools_options.yaml
  35. +3
    -0
      gradle.properties
  36. +14
    -0
      ios/Flutter/Generated.xcconfig
  37. +13
    -0
      ios/Flutter/flutter_export_environment.sh
  38. +48
    -0
      ios/Podfile
  39. +19
    -0
      ios/Runner/GeneratedPluginRegistrant.h
  40. +63
    -0
      ios/Runner/GeneratedPluginRegistrant.m
  41. BIN
      lib.zip
  42. +101
    -0
      lib/core/theme/app_theme.dart
  43. +42
    -0
      lib/core/theme/colors.dart
  44. +94
    -0
      lib/data/models/content_post.dart
  45. +77
    -0
      lib/data/models/user_profile.dart
  46. +175
    -0
      lib/data/services/api_service.dart
  47. +73
    -0
      lib/data/services/auth_service.dart
  48. +95
    -0
      lib/data/services/image_service.dart
  49. +153
    -0
      lib/main.dart
  50. +270
    -0
      lib/presentation/screens/ai_enhancement/ai_enhancement_screen.dart
  51. +29
    -0
      lib/presentation/screens/export/export_screen.dart
  52. +54
    -0
      lib/presentation/screens/home/home_screen.dart
  53. +58
    -0
      lib/presentation/screens/home/login_screen.dart
  54. +79
    -0
      lib/presentation/screens/image_preview/image_preview_screen.dart
  55. +147
    -0
      lib/presentation/screens/media_picker/media_picker_screen.dart
  56. +131
    -0
      lib/presentation/screens/post_preview/post_preview_screen.dart
  57. +175
    -0
      lib/presentation/screens/post_refinement/post_refinement_screen.dart
  58. +206
    -0
      lib/presentation/screens/profile_setup/profile_setup_screen.dart
  59. +267
    -0
      lib/presentation/screens/text_generation/text_generation_screen.dart
  60. +108
    -0
      lib/presentation/widgets/creation_flow_layout.dart
  61. +94
    -0
      lib/repositories/ai_repository.dart
  62. +30
    -0
      lib/routes/app_routes.dart
  63. +250
    -0
      lib/services/gemini_service.dart
  64. +57
    -0
      lib/services/image_analysis_service.dart
  65. +9
    -0
      lib/services/image_editing_service.dart
  66. +196
    -0
      lib/services/ollama_service.dart
  67. +71
    -0
      lib/services/post_creation_service.dart
  68. +138
    -0
      lib/services/stable_diffusion_service.dart
  69. +60
    -0
      lib/services/text_improvment_service.dart
  70. +7
    -0
      local.properties
  71. +1114
    -0
      pubspec.lock
  72. +69
    -0
      pubspec.yaml
  73. +30
    -0
      test/widget_test.dart

+ 78
- 0
.gitignore Переглянути файл

@@ -0,0 +1,78 @@
# 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

+ 30
- 0
.metadata Переглянути файл

@@ -0,0 +1,30 @@
# 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'

+ 116
- 0
README.md Переглянути файл

@@ -0,0 +1,116 @@
# 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**

+ 170
- 0
analysis_options.yaml Переглянути файл

@@ -0,0 +1,170 @@
# 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

+ 14
- 0
android/.gitignore Переглянути файл

@@ -0,0 +1,14 @@
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

+ 54
- 0
android/app/build.gradle.kts Переглянути файл

@@ -0,0 +1,54 @@
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")
}

+ 14
- 0
android/app/proguard-rules.pro Переглянути файл

@@ -0,0 +1,14 @@
# 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

+ 7
- 0
android/app/src/debug/AndroidManifest.xml Переглянути файл

@@ -0,0 +1,7 @@
<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>

+ 53
- 0
android/app/src/main/AndroidManifest.xml Переглянути файл

@@ -0,0 +1,53 @@
<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>

+ 12
- 0
android/app/src/main/kotlin/com/example/social_content_creator/MainActivity.kt Переглянути файл

@@ -0,0 +1,12 @@
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)
}
}

+ 12
- 0
android/app/src/main/res/drawable-v21/launch_background.xml Переглянути файл

@@ -0,0 +1,12 @@
<?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>

+ 12
- 0
android/app/src/main/res/drawable/launch_background.xml Переглянути файл

@@ -0,0 +1,12 @@
<?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>

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png Переглянути файл

Before After
Width: 72  |  Height: 72  |  Size: 544B

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png Переглянути файл

Before After
Width: 48  |  Height: 48  |  Size: 442B

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png Переглянути файл

Before After
Width: 96  |  Height: 96  |  Size: 721B

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png Переглянути файл

Before After
Width: 144  |  Height: 144  |  Size: 1.0KB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png Переглянути файл

Before After
Width: 192  |  Height: 192  |  Size: 1.4KB

+ 18
- 0
android/app/src/main/res/values-night/styles.xml Переглянути файл

@@ -0,0 +1,18 @@
<?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>

+ 18
- 0
android/app/src/main/res/values/styles.xml Переглянути файл

@@ -0,0 +1,18 @@
<?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>

+ 6
- 0
android/app/src/main/res/xml/file_paths.xml Переглянути файл

@@ -0,0 +1,6 @@
<?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>

+ 7
- 0
android/app/src/profile/AndroidManifest.xml Переглянути файл

@@ -0,0 +1,7 @@
<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>

+ 20
- 0
android/build.gradle.kts Переглянути файл

@@ -0,0 +1,20 @@
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)
}

+ 8
- 0
android/gradle.properties Переглянути файл

@@ -0,0 +1,8 @@
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

+ 5
- 0
android/gradle/wrapper/gradle-wrapper.properties Переглянути файл

@@ -0,0 +1,5 @@
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

BIN
android/java_pid15420.hprof Переглянути файл


+ 37
- 0
android/settings.gradle.kts Переглянути файл

@@ -0,0 +1,37 @@
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")

+ 0
- 0
assets/fonts/.gitkeep Переглянути файл


BIN
assets/fonts/Inter-Bold.ttf Переглянути файл


BIN
assets/fonts/Inter-Regular.ttf Переглянути файл


BIN
assets/fonts/Inter-SemiBold.ttf Переглянути файл


+ 0
- 0
assets/icons/.gitkeep Переглянути файл


+ 0
- 0
assets/images/.gitkeep Переглянути файл


BIN
assets/images/placeholder.jpg Переглянути файл

Before After
Width: 1024  |  Height: 1024  |  Size: 90KB

+ 3
- 0
devtools_options.yaml Переглянути файл

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

+ 3
- 0
gradle.properties Переглянути файл

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4096m
android.useAndroidX=true
android.enableJetifier=true

+ 14
- 0
ios/Flutter/Generated.xcconfig Переглянути файл

@@ -0,0 +1,14 @@
// 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

+ 13
- 0
ios/Flutter/flutter_export_environment.sh Переглянути файл

@@ -0,0 +1,13 @@
#!/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"

+ 48
- 0
ios/Podfile Переглянути файл

@@ -0,0 +1,48 @@
# 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

+ 19
- 0
ios/Runner/GeneratedPluginRegistrant.h Переглянути файл

@@ -0,0 +1,19 @@
//
// 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 */

+ 63
- 0
ios/Runner/GeneratedPluginRegistrant.m Переглянути файл

@@ -0,0 +1,63 @@
//
// 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


+ 101
- 0
lib/core/theme/app_theme.dart Переглянути файл

@@ -0,0 +1,101 @@
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),
);
}
}

+ 42
- 0
lib/core/theme/colors.dart Переглянути файл

@@ -0,0 +1,42 @@
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);
}

+ 94
- 0
lib/data/models/content_post.dart Переглянути файл

@@ -0,0 +1,94 @@
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,
});

+ 77
- 0
lib/data/models/user_profile.dart Переглянути файл

@@ -0,0 +1,77 @@
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})';
}

+ 175
- 0
lib/data/services/api_service.dart Переглянути файл

@@ -0,0 +1,175 @@
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.',
];
}
}
}

+ 73
- 0
lib/data/services/auth_service.dart Переглянути файл

@@ -0,0 +1,73 @@
// 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");
}
}
}

+ 95
- 0
lib/data/services/image_service.dart Переглянути файл

@@ -0,0 +1,95 @@
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';
}

+ 153
- 0
lib/main.dart Переглянути файл

@@ -0,0 +1,153 @@
// 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),
)),
),
);
}
}

+ 270
- 0
lib/presentation/screens/ai_enhancement/ai_enhancement_screen.dart Переглянути файл

@@ -0,0 +1,270 @@
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(),
),
),
),
),
);
}),
);
}
}

+ 29
- 0
lib/presentation/screens/export/export_screen.dart Переглянути файл

@@ -0,0 +1,29 @@
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'),
),
],
),
),
);
}
}

+ 54
- 0
lib/presentation/screens/home/home_screen.dart Переглянути файл

@@ -0,0 +1,54 @@
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),
),
)
],
),
),
);
}
}

+ 58
- 0
lib/presentation/screens/home/login_screen.dart Переглянути файл

@@ -0,0 +1,58 @@
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),
),
),
),
);
}
}

+ 79
- 0
lib/presentation/screens/image_preview/image_preview_screen.dart Переглянути файл

@@ -0,0 +1,79 @@
// 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,
),
),
);
}
}
}

+ 147
- 0
lib/presentation/screens/media_picker/media_picker_screen.dart Переглянути файл

@@ -0,0 +1,147 @@
// 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)),
],
),
),
),
],
),
);
}
}

+ 131
- 0
lib/presentation/screens/post_preview/post_preview_screen.dart Переглянути файл

@@ -0,0 +1,131 @@
// 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),
),
),
),
],
),
),
);
}
}

+ 175
- 0
lib/presentation/screens/post_refinement/post_refinement_screen.dart Переглянути файл

@@ -0,0 +1,175 @@
// 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)),
),
],
),
),
),
);
}
}

+ 206
- 0
lib/presentation/screens/profile_setup/profile_setup_screen.dart Переглянути файл

@@ -0,0 +1,206 @@
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(),
),
],
),
),
);
}
}

+ 267
- 0
lib/presentation/screens/text_generation/text_generation_screen.dart Переглянути файл

@@ -0,0 +1,267 @@
// 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
),
);

},
),
),
),
),
],
),
);
}
}

+ 108
- 0
lib/presentation/widgets/creation_flow_layout.dart Переглянути файл

@@ -0,0 +1,108 @@
// 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,
),
],
),
);
}
}

+ 94
- 0
lib/repositories/ai_repository.dart Переглянути файл

@@ -0,0 +1,94 @@
// 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,
);
}
}

+ 30
- 0
lib/routes/app_routes.dart Переглянути файл

@@ -0,0 +1,30 @@
// 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(),
};
*/
}

+ 250
- 0
lib/services/gemini_service.dart Переглянути файл

@@ -0,0 +1,250 @@
// 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 '';
}
}
}

+ 57
- 0
lib/services/image_analysis_service.dart Переглянути файл

@@ -0,0 +1,57 @@
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;
}
}
}

+ 9
- 0
lib/services/image_editing_service.dart Переглянути файл

@@ -0,0 +1,9 @@
// 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});
}

+ 196
- 0
lib/services/ollama_service.dart Переглянути файл

@@ -0,0 +1,196 @@
// 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;
}
}
}


+ 71
- 0
lib/services/post_creation_service.dart Переглянути файл

@@ -0,0 +1,71 @@
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;
}
}
}

+ 138
- 0
lib/services/stable_diffusion_service.dart Переглянути файл

@@ -0,0 +1,138 @@
// 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));
}
}

+ 60
- 0
lib/services/text_improvment_service.dart Переглянути файл

@@ -0,0 +1,60 @@
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;
}
}
}

+ 7
- 0
local.properties Переглянути файл

@@ -0,0 +1,7 @@
# 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

+ 1114
- 0
pubspec.lock
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 69
- 0
pubspec.yaml Переглянути файл

@@ -0,0 +1,69 @@
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

+ 30
- 0
test/widget_test.dart Переглянути файл

@@ -0,0 +1,30 @@
// 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);
});
}

Завантаження…
Відмінити
Зберегти