diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7d1b4e7 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +API_URL_ANDROID=http://10.0.2.2:8000 +API_URL_IOS=http://localhost:8000 +API_URL_PHYSICAL= +URL_MEDIAS= +API_TIMEOUT=10 \ No newline at end of file diff --git a/.flutter-plugins-dependencies 2 b/.flutter-plugins-dependencies 2 new file mode 100644 index 0000000..e7f8482 --- /dev/null +++ b/.flutter-plugins-dependencies 2 @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"audioplayers_darwin","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/audioplayers_darwin-6.2.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"video_player_avfoundation","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/video_player_avfoundation-2.7.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"audioplayers_android","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/audioplayers_android-5.2.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/path_provider_android-2.2.16/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"video_player_android","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/video_player_android-2.8.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"audioplayers_darwin","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/audioplayers_darwin-6.2.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"video_player_avfoundation","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/video_player_avfoundation-2.7.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"audioplayers_linux","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/audioplayers_linux-4.2.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"audioplayers_windows","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/audioplayers_windows-4.2.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"audioplayers_web","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/audioplayers_web-5.1.0/","dependencies":[],"dev_dependency":false},{"name":"video_player_web","path":"/Users/elhatriayoub/.pub-cache/hosted/pub.dev/video_player_web-2.3.4/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"audioplayers","dependencies":["audioplayers_android","audioplayers_darwin","audioplayers_linux","audioplayers_web","audioplayers_windows","path_provider"]},{"name":"audioplayers_android","dependencies":[]},{"name":"audioplayers_darwin","dependencies":[]},{"name":"audioplayers_linux","dependencies":[]},{"name":"audioplayers_web","dependencies":[]},{"name":"audioplayers_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"video_player","dependencies":["video_player_android","video_player_avfoundation","video_player_web"]},{"name":"video_player_android","dependencies":[]},{"name":"video_player_avfoundation","dependencies":[]},{"name":"video_player_web","dependencies":[]}],"date_created":"2026-02-15 15:34:16.509165","version":"3.35.5","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 924e127..7df6ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related @@ -42,4 +44,6 @@ app.*.map.json /android/app/profile /android/app/release -build \ No newline at end of file + +build +.env \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 107258c..bba7235 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,9 +6,9 @@ plugins { } android { - namespace = "com.example.seriouse_game" - compileSdk = 35 //flutter.compileSdkVersion - ndkVersion = "25.1.8937393" //flutter.ndkVersion + namespace = "com.example.factoscope" + compileSdk = 36 //flutter.compileSdkVersion + ndkVersion = "26.1.10909125" //flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -21,7 +21,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.seriouse_game" + applicationId = "com.example.factoscope" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b326570..fb4074e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round"> + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> - - + + - - + + - + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/projet_collective/MainActivity.kt b/android/app/src/main/kotlin/com/example/projet_collective/MainActivity.kt index 2528f66..390f4fd 100644 --- a/android/app/src/main/kotlin/com/example/projet_collective/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/projet_collective/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.seriouse_game +package com.example.factoscope import io.flutter.embedding.android.FlutterActivity diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c9ad5f9 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..c9ad5f9 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f5ae6d2 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b7..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f5ae6d2 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f5ae6d2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f5ae6d2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372e..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f5ae6d2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index cb1ef88..cb9786d 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,6 +1,7 @@ - + - + + #F0AE00 + \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7bb2df6..3c85cfe 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index a42444d..4f52071 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.1" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/assets/icon/icon_android.png b/assets/icon/icon_android.png new file mode 100644 index 0000000..252dfb1 Binary files /dev/null and b/assets/icon/icon_android.png differ diff --git a/ios/Flutter/Generated 2.xcconfig b/ios/Flutter/Generated 2.xcconfig new file mode 100644 index 0000000..4744a3b --- /dev/null +++ b/ios/Flutter/Generated 2.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/opt/homebrew/share/flutter +FLUTTER_APPLICATION_PATH=/Users/elhatriayoub/Desktop/PCOl +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 diff --git a/ios/Flutter/flutter_export_environment 2.sh b/ios/Flutter/flutter_export_environment 2.sh new file mode 100755 index 0000000..5299fd4 --- /dev/null +++ b/ios/Flutter/flutter_export_environment 2.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/opt/homebrew/share/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/elhatriayoub/Desktop/PCOl" +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" diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index c491f5e..e5c700d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - seriouse_game + factoscope CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/DataBase/database_helper.dart b/lib/DataBase/database_helper.dart deleted file mode 100644 index df5a889..0000000 --- a/lib/DataBase/database_helper.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:sqflite/sqflite.dart'; -import 'package:path/path.dart'; - -class DatabaseHelper { - static final DatabaseHelper instance = DatabaseHelper._init(); - static Database? _database; - - DatabaseHelper._init(); - - // Méthode pour obtenir la base de données - Future get database async { - if (_database != null) return _database!; - _database = await _initDB('app.db'); - return _database!; - } - - // Initialisation de la base de données - Future _initDB(String filePath) async { - final dbPath = await getDatabasesPath(); - final path = join(dbPath, filePath); - //resetDatabase(); - return await openDatabase( - path, - version: 1, - onOpen: (db) async { - // Appeler _createDB pour recréer les tables si nécessaire - await _createDB(db, 1); - }, - ); - } - - // Crée les tables dans la base de données - Future _createDB(Database db, int version) async { - await db.execute(''' - CREATE TABLE IF NOT EXISTS module ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - urlImg TEXT NOT NULL, - titre TEXT NOT NULL, - description TEXT NOT NULL - ); - '''); - - await db.execute(''' - CREATE TABLE IF NOT EXISTS cours ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - titre TEXT NOT NULL, - contenu TEXT NOT NULL, - id_module INTEGER, - FOREIGN KEY (id_module) REFERENCES module (id) - ); - '''); - - await db.execute(''' - CREATE TABLE IF NOT EXISTS page ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - description TEXT , - ordre INTEGER NOT NULL, - urlAudio TEST DEFAULT "", - est_vue INTEGER DEFAULT 0, - id_cours INTEGER NOT NULL, - FOREIGN KEY (id_cours) REFERENCES cours (id) ON DELETE CASCADE ON UPDATE CASCADE - ); - '''); - - await db.execute(''' - CREATE TABLE IF NOT EXISTS MiniJeu ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - id_cours INTEGER NOT NULL, - nom TEXT NOT NULL, - description TEXT, - progression INTEGER NOT NULL, - FOREIGN KEY (id_cours) REFERENCES cours (id) - ); -'''); - - await db.execute(''' - CREATE TABLE IF NOT EXISTS MotsCroises ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - id_minijeu INTEGER NOT NULL, - taille_grille TEXT NOT NULL, - description TEXT, - FOREIGN KEY (id_minijeu) REFERENCES MiniJeu (id) ON DELETE CASCADE - - ); - '''); - - await db.execute(''' - CREATE TABLE IF NOT EXISTS Mot ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - id_motscroises INTEGER NOT NULL, - mot TEXT NOT NULL, - indice TEXT NOT NULL, - direction TEXT NOT NULL, - position_depart_x INTEGER NOT NULL, - position_depart_y INTEGER NOT NULL, - FOREIGN KEY (id_motscroises) REFERENCES MotsCroises (id) ON DELETE CASCADE - ); - '''); - - await db.execute(''' - CREATE TABLE IF NOT EXISTS MediaCours ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - id_page INTEGER NOT NULL, - ordre INTEGER NOT NULL, - url TEXT NOT NULL, - type TEXT NOT NULL, - caption TEXT, - FOREIGN KEY (id_page) REFERENCES page (id) ON DELETE CASCADE - ); -'''); - await db.execute(''' - CREATE TABLE IF NOT EXISTS ObjectifCours ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - id_cours INTEGER NOT NULL, - description TEXT NOT NULL, - FOREIGN KEY (id_cours) REFERENCES cours (id) ON DELETE CASCADE - ); -'''); - - await db.execute(''' -CREATE TABLE Question( - idQuestion INTEGER, - PRIMARY KEY(idQuestion) -);'''); - - await db.execute('''CREATE TABLE QuestionImg( - idQuestion INTEGER, - urlImage TEXT NOT NULL, - caption TEXT NOT NULL, - PRIMARY KEY(idQuestion), - FOREIGN KEY(idQuestion) REFERENCES Question(idQuestion) -);'''); - - await db.execute('''CREATE TABLE QuestionText( - idQuestion INTEGER, - txt TEXT NOT NULL, - PRIMARY KEY(idQuestion), - FOREIGN KEY(idQuestion) REFERENCES Question(idQuestion) -);'''); - - await db.execute('''CREATE TABLE QCM( - idQCM INTEGER, - numSolution INTEGER NOT NULL, - idCours INTEGER NOT NULL, - idQuestion INTEGER NOT NULL, - PRIMARY KEY(idQCM), - UNIQUE(idQuestion), - FOREIGN KEY(idCours) REFERENCES cours(id), - FOREIGN KEY(idQuestion) REFERENCES Question(idQuestion) -);'''); - - await db.execute('''CREATE TABLE Reponse( - idReponse INTEGER, - idQCM INTEGER NOT NULL, - PRIMARY KEY(idReponse), - FOREIGN KEY(idQCM) REFERENCES QCM(idQCM) -);'''); - - await db.execute('''CREATE TABLE ReponseImg( - idReponse INTEGER, - urlImage TEXT NOT NULL, - caption TEXT NOT NULL, - PRIMARY KEY(idReponse), - FOREIGN KEY(idReponse) REFERENCES Reponse(idReponse) -);'''); - - await db.execute('''CREATE TABLE ReponseText( - idReponse INTEGER, - txt TEXT NOT NULL, - PRIMARY KEY(idReponse), - FOREIGN KEY(idReponse) REFERENCES Reponse(idReponse) -);'''); - - - } - - // Supprime la base de données et recrée les tables - Future resetDatabase() async { - final dbPath = await getDatabasesPath(); - final path = join(dbPath, 'app.db'); - - // Supprime la base de données existante - await deleteDatabase(path); - - // Rouvre la base de données pour recréer les tables - _database = await _initDB('app.db'); - } - - // Méthode pour fermer la base de données - Future close() async { - final db = _database; - if (db != null) { - await db.close(); - } - } -} diff --git a/lib/config/Couleurs et typos.md b/lib/assets/Couleurs et typos.md similarity index 100% rename from lib/config/Couleurs et typos.md rename to lib/assets/Couleurs et typos.md diff --git a/lib/assets/cfi.jpg b/lib/assets/cfi.jpg new file mode 100644 index 0000000..86ae4d8 Binary files /dev/null and b/lib/assets/cfi.jpg differ diff --git a/lib/assets/epjt.png b/lib/assets/epjt.png new file mode 100644 index 0000000..eb32512 Binary files /dev/null and b/lib/assets/epjt.png differ diff --git a/lib/data/AppData/CharteFactoscope/facto-education.png b/lib/assets/facto-education.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/facto-education.png rename to lib/assets/facto-education.png diff --git a/lib/data/AppData/CharteFactoscope/facto-environnement.png b/lib/assets/facto-environnement.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/facto-environnement.png rename to lib/assets/facto-environnement.png diff --git a/lib/data/AppData/CharteFactoscope/facto-institutions.png b/lib/assets/facto-institutions.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/facto-institutions.png rename to lib/assets/facto-institutions.png diff --git a/lib/data/AppData/facto-logo.png b/lib/assets/facto-logo.png similarity index 100% rename from lib/data/AppData/facto-logo.png rename to lib/assets/facto-logo.png diff --git a/lib/data/AppData/CharteFactoscope/facto-politique.png b/lib/assets/facto-politique.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/facto-politique.png rename to lib/assets/facto-politique.png diff --git a/lib/data/AppData/CharteFactoscope/facto-sante.png b/lib/assets/facto-sante.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/facto-sante.png rename to lib/assets/facto-sante.png diff --git a/lib/data/AppData/CharteFactoscope/facto-securite.png b/lib/assets/facto-securite.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/facto-securite.png rename to lib/assets/facto-securite.png diff --git a/lib/data/AppData/CharteFactoscope/facto-societe.png b/lib/assets/facto-societe.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/facto-societe.png rename to lib/assets/facto-societe.png diff --git a/lib/data/AppData/goals.png b/lib/assets/goals.png similarity index 100% rename from lib/data/AppData/goals.png rename to lib/assets/goals.png diff --git a/lib/data/AppData/CharteFactoscope/logo-factoscope.png b/lib/assets/logo-factoscope.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/logo-factoscope.png rename to lib/assets/logo-factoscope.png diff --git a/lib/data/AppData/CharteFactoscope/logo-factoscope_seul.png b/lib/assets/logo-factoscope_seul.png similarity index 100% rename from lib/data/AppData/CharteFactoscope/logo-factoscope_seul.png rename to lib/assets/logo-factoscope_seul.png diff --git a/lib/assets/logo-factoscope_seul_2.png b/lib/assets/logo-factoscope_seul_2.png new file mode 100644 index 0000000..b7d1ab3 Binary files /dev/null and b/lib/assets/logo-factoscope_seul_2.png differ diff --git a/lib/assets/nothing2hide.jpg b/lib/assets/nothing2hide.jpg new file mode 100644 index 0000000..b741dec Binary files /dev/null and b/lib/assets/nothing2hide.jpg differ diff --git a/lib/config.dart b/lib/config.dart new file mode 100644 index 0000000..19fe283 --- /dev/null +++ b/lib/config.dart @@ -0,0 +1,25 @@ +import 'dart:io'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class AppConfig { + static String get apiBaseUrlAndroidEmulator => + dotenv.env['API_URL_ANDROID'] ?? 'http://10.0.2.2:8000'; + + static String get apiBaseUrlIosSimulator => + dotenv.env['API_URL_IOS'] ?? 'http://localhost:8000'; + + static String get apiBaseUrlPhysicalDevice => + dotenv.env['API_URL_PHYSICAL'] ?? 'http://192.168.1.X:8000'; + + static String get urlMedias => + dotenv.env['URL_MEDIAS'] ?? ''; + + static int get apiTimeout => + int.tryParse(dotenv.env['API_TIMEOUT'] ?? '"10') ?? 10; + + static String get effectiveApiUrl { + if (Platform.isAndroid) return apiBaseUrlAndroidEmulator; + if (Platform.isIOS) return apiBaseUrlIosSimulator; + return apiBaseUrlAndroidEmulator; + } +} \ No newline at end of file diff --git a/lib/data/AppData/CharteFactoscope/Couleurs et typos.md b/lib/data/AppData/CharteFactoscope/Couleurs et typos.md deleted file mode 100644 index 2903655..0000000 --- a/lib/data/AppData/CharteFactoscope/Couleurs et typos.md +++ /dev/null @@ -1,27 +0,0 @@ -## Couleurs - -#### jaune - -#fcb330 - -#### bleu - -#292460 - -#### écriture  - -#666 - -## Typos  - -#### titres - - - -#### highlights - - - -#### texte - - \ No newline at end of file diff --git a/lib/data/AppData/CharteFactoscope/logo-factoscope_seul_2.png b/lib/data/AppData/CharteFactoscope/logo-factoscope_seul_2.png deleted file mode 100644 index 0b2ce7c..0000000 Binary files a/lib/data/AppData/CharteFactoscope/logo-factoscope_seul_2.png and /dev/null differ diff --git a/lib/data/AppData/Module1/metadata.json b/lib/data/AppData/Module1/metadata.json new file mode 100644 index 0000000..9b676f0 --- /dev/null +++ b/lib/data/AppData/Module1/metadata.json @@ -0,0 +1,5 @@ +{ + "titre": "Module 1", + "urlImg": "assets/facto-societe.png", + "description": "Chaque citoyen a un rôle à jouer en matière de lutte contre la désinformation… À condition qu’il maîtrise les codes de son environnement informationnel : les sources à sa disposition, les fondements du travail journalistique, les rouages des réseaux sociaux numériques, les risques désinformationnels, etc. A nous tous de nous emparer de ces connaissances pour exercer pleinement et librement nos droits et devoirs de citoyens !" +} \ No newline at end of file diff --git a/lib/data/AppData/Module2/metadata.json b/lib/data/AppData/Module2/metadata.json new file mode 100644 index 0000000..eee9b09 --- /dev/null +++ b/lib/data/AppData/Module2/metadata.json @@ -0,0 +1,5 @@ +{ + "titre": "Module 2", + "urlImg": "assets/facto-societe.png", + "description": "Chaque citoyen a un rôle à jouer en matière de lutte contre la désinformation… À condition qu’il maîtrise les codes de son environnement informationnel : les sources à sa disposition, les fondements du travail journalistique, les rouages des réseaux sociaux numériques, les risques désinformationnels, etc. A nous tous de nous emparer de ces connaissances pour exercer pleinement et librement nos droits et devoirs de citoyens !" +} \ No newline at end of file diff --git a/lib/data/AppData/Module3/metadata.json b/lib/data/AppData/Module3/metadata.json new file mode 100644 index 0000000..c16ce7c --- /dev/null +++ b/lib/data/AppData/Module3/metadata.json @@ -0,0 +1,5 @@ +{ + "titre": "Module 3", + "urlImg": "assets/facto-societe.png", + "description": "Chaque citoyen a un rôle à jouer en matière de lutte contre la désinformation… À condition qu’il maîtrise les codes de son environnement informationnel : les sources à sa disposition, les fondements du travail journalistique, les rouages des réseaux sociaux numériques, les risques désinformationnels, etc. A nous tous de nous emparer de ces connaissances pour exercer pleinement et librement nos droits et devoirs de citoyens !" +} \ No newline at end of file diff --git a/lib/data/AppData/facto-societe.png b/lib/data/AppData/facto-societe.png deleted file mode 100644 index 7aca33f..0000000 Binary files a/lib/data/AppData/facto-societe.png and /dev/null differ diff --git a/lib/data/AppData/music.mp3 b/lib/data/AppData/music.mp3 deleted file mode 100644 index 9201688..0000000 Binary files a/lib/data/AppData/music.mp3 and /dev/null differ diff --git a/lib/data/AppData/ok.txt b/lib/data/AppData/ok.txt deleted file mode 100644 index 1bd8ee3..0000000 --- a/lib/data/AppData/ok.txt +++ /dev/null @@ -1 +0,0 @@ -etzteoajhtejrtyeatyear_t \ No newline at end of file diff --git a/lib/data/AppData/test.mp4 b/lib/data/AppData/test.mp4 deleted file mode 100644 index a4bf26b..0000000 Binary files a/lib/data/AppData/test.mp4 and /dev/null differ diff --git a/lib/data/ArchiveBestPattern2QCM/question.txt b/lib/data/ArchiveBestPattern2QCM/question.txt deleted file mode 100644 index e61ca26..0000000 --- a/lib/data/ArchiveBestPattern2QCM/question.txt +++ /dev/null @@ -1 +0,0 @@ -Qu'elle est la couleur du ciel ? \ No newline at end of file diff --git a/lib/data/ArchiveBestPattern2QCM/reponse1.txt b/lib/data/ArchiveBestPattern2QCM/reponse1.txt deleted file mode 100644 index 2013e49..0000000 --- a/lib/data/ArchiveBestPattern2QCM/reponse1.txt +++ /dev/null @@ -1 +0,0 @@ -vert \ No newline at end of file diff --git a/lib/data/ArchiveBestPattern2QCM/reponse2.txt b/lib/data/ArchiveBestPattern2QCM/reponse2.txt deleted file mode 100644 index aec3c84..0000000 --- a/lib/data/ArchiveBestPattern2QCM/reponse2.txt +++ /dev/null @@ -1 +0,0 @@ -bleu \ No newline at end of file diff --git a/lib/data/ArchiveBestPattern2QCM/reponse3.txt b/lib/data/ArchiveBestPattern2QCM/reponse3.txt deleted file mode 100644 index 5923218..0000000 --- a/lib/data/ArchiveBestPattern2QCM/reponse3.txt +++ /dev/null @@ -1 +0,0 @@ -jaune \ No newline at end of file diff --git a/lib/data/ArchiveBestPattern2QCM/reponse4.txt b/lib/data/ArchiveBestPattern2QCM/reponse4.txt deleted file mode 100644 index e672b18..0000000 --- a/lib/data/ArchiveBestPattern2QCM/reponse4.txt +++ /dev/null @@ -1 +0,0 @@ -je ne sais pas ! \ No newline at end of file diff --git a/lib/data/ArchiveBestPattern2QCM/solution.txt b/lib/data/ArchiveBestPattern2QCM/solution.txt deleted file mode 100644 index d8263ee..0000000 --- a/lib/data/ArchiveBestPattern2QCM/solution.txt +++ /dev/null @@ -1 +0,0 @@ -2 \ No newline at end of file diff --git a/lib/data_initializer.dart b/lib/data_initializer.dart index 5ea703f..c35fdc4 100644 --- a/lib/data_initializer.dart +++ b/lib/data_initializer.dart @@ -1,591 +1,325 @@ +import 'package:flutter/services.dart'; +import 'package:factoscope/models/module.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/repositories/cours_repository.dart'; +import 'package:factoscope/repositories/module_repository.dart'; +import 'package:factoscope/repositories/page_repository.dart'; +import 'package:factoscope/ui/module_selectionne.dart'; +import 'package:factoscope/ui/cours_selectionne.dart'; +import 'package:factoscope/database_helper.dart'; import 'package:flutter/foundation.dart'; -import 'package:seriouse_game/models/module.dart'; -import 'package:seriouse_game/repositories/coursRepository.dart'; - -import 'package:seriouse_game/repositories/moduleRepository.dart'; -import 'package:seriouse_game/repositories/minijeuRepository.dart'; -import 'package:seriouse_game/repositories/mediaCoursRepository.dart'; -import 'package:seriouse_game/repositories/objectifCoursRepository.dart'; -import 'package:seriouse_game/repositories/pageRepository.dart'; - -import 'package:seriouse_game/repositories/QCM/QCMRepository.dart'; -import 'package:seriouse_game/repositories/QCM/QuestionRepository.dart'; -import 'package:seriouse_game/repositories/QCM/ReponseRepository.dart'; -import 'package:seriouse_game/ui/ModuleSelectionne.dart'; - -import 'DataBase/database_helper.dart'; -import 'models/cours.dart'; -import 'models/mediaCours.dart'; -import 'models/objectifCours.dart'; -import 'models/page.dart'; - -import 'models/QCM/qcm.dart'; -import 'models/QCM/question.dart'; -import 'models/QCM/reponse.dart'; - -import 'package:seriouse_game/ui/CoursSelectionne.dart'; +import 'dart:convert'; final moduleRepository = ModuleRepository(); - final coursRepository = CoursRepository(); - final miniJeuRepository = MiniJeuRepository(); - final mediaCoursRepository = MediaCoursRepository(); - final pageRepository = PageRepository(); - final objectifCoursRepository = ObjectifCoursRepository(); - - final questionRepo = QuestionRepository(); - final reponseRepo = ReponseRepository(); - final qcmRepo = QCMRepository(); +final coursRepository = CoursRepository(); +final pageRepository = PageRepository(); Future insertModule1() async { - // Création du Module - final module = Module( - titre: 'Thématique 1', - urlImg: 'lib/data/AppData/facto-societe.png', - description: 'Chaque citoyen a un rôle à jouer en matière de lutte contre la désinformation… À condition qu’il maîtrise les codes de son environnement informationnel : les sources à sa disposition, les fondements du travail journalistique, les rouages des réseaux sociaux numériques, les risques désinformationnels, etc. A nous tous de nous emparer de ces connaissances pour exercer pleinement et librement nos droits et devoirs de citoyens !'); - final moduleId = await moduleRepository.create(module); - - // Création du Cours - Cours cours = Cours( - idModule: moduleId, - titre: 'Les sources d’informations', - contenu: 'Comprendre et évaluer les sources d’information.'); - final coursId = await coursRepository.create(cours); - - // Ajout des Objectifs du Cours - final objectif1 = ObjectifCours( - idCours: coursId, - description: 'Comprendre les différents types de sources d’information', - ); - final objectif2 = ObjectifCours( - idCours: coursId, - description: 'Savoir évaluer la crédibilité d’une source', - ); - final objectif3 = ObjectifCours( - idCours: coursId, - description: 'Identifier les signes de désinformation', - ); - - await objectifCoursRepository.create(objectif1); - await objectifCoursRepository.create(objectif2); - await objectifCoursRepository.create(objectif3); - - // Page 1 : Introduction aux sources d'information - Page page1 = Page(idCours: coursId, ordre: 1, description: "Qu'est-ce qu'une source d'information ?"); - int pageId1 = await pageRepository.create(page1); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId1, - ordre: 1, - url: 'lib/data/AppData/Module1/Cours1/source_information_definition.txt', - type: 'text')); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId1, - ordre: 2, - url: 'lib/data/AppData/Module1/Cours1/journaliste_interview.jpg', - type: 'image', - caption: 'Journaliste réalisant une interview')); - - // Page 2 : Les différentes sources - Page page2 = Page(idCours: coursId, ordre: 2, description: "Types de sources d'information"); - int pageId2 = await pageRepository.create(page2); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId2, - ordre: 1, - url: 'lib/data/AppData/Module1/Cours1/types_sources.txt', - type: 'text')); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId2, - ordre: 2, - url: 'lib/data/AppData/Module1/Cours1/source_primaire_secondaire.png', - type: 'image', - caption: 'Illustration des sources primaires et secondaires')); - - // Page 3 : Évaluer la crédibilité d'une source - Page page3 = Page(idCours: coursId, ordre: 3, description: "Comment vérifier la fiabilité d'une source ?"); - int pageId3 = await pageRepository.create(page3); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId3, - ordre: 1, - url: 'lib/data/AppData/Module1/Cours1/evaluer_sources.txt', - type: 'text')); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId3, - ordre: 2, - url: 'lib/data/AppData/Module1/Cours1/fake_news_verification.png', - type: 'image', - caption: 'Techniques de vérification des fake news')); - - /// Données de test pour les QCM - List testQCMs = [ - QCM( - id: 1, - numSolution: 2, - idCours: 1, - idQuestion: 1, - question: - Question( - id: 1, - text: "Quel est le principal indicateur de la fiabilité d’une source d’information ?", - type: "text", - ), - - reponses: [ - Reponse(id: 1, idQCM: 1, text: "Sa popularité sur les réseaux sociaux", type: "text"), - Reponse(id: 2, idQCM: 1, text: "La vérifiabilité des informations par d’autres sources fiables", type: "text"), - Reponse(id: 3, idQCM: 1, text: "Le nombre de commentaires sous l’article", type: "text"), - Reponse(id: 4, idQCM: 1, text: "Le design du site web", type: "text"), - ], - ), - QCM( - id: 2, - numSolution: 2, - idCours: 1, - idQuestion: 2, - question: - Question( - id: 2, - text: "Quelle est la meilleure manière de vérifier une information trouvée en ligne ?", - type: "text", - ), - - reponses: [ - Reponse(id: 5, idQCM: 2, text: "La partager immédiatement avec ses amis", type: "text"), - Reponse(id: 6, idQCM: 2, text: "Consulter plusieurs sources fiables et vérifier la cohérence de l’information", type: "text"), - Reponse(id: 7, idQCM: 2, text: "Faire confiance à la première source trouvée", type: "text"), - Reponse(id: 8, idQCM: 2, text: "Vérifier si l’information est amusante avant de la croire", type: "text"), - ], - ), - QCM( - id: 3, - numSolution: 2, - idCours: 1, - idQuestion: 3, - question: - Question( - id: 3, - text: "Quel est un signe révélateur d’une fausse information ?", - type: "text", - ), - - reponses: [ - Reponse(id: 9, idQCM: 3, text: "Elle provient d’un média reconnu et sérieux", type: "text"), - Reponse(id: 10, idQCM: 3, text: "Elle utilise un ton sensationnaliste et manque de sources vérifiables", type: "text"), - Reponse(id: 11, idQCM: 3, text: "Elle cite plusieurs experts et références", type: "text"), - Reponse(id: 12, idQCM: 3, text: "Elle est reprise par plusieurs médias de confiance", type: "text"), - ], - ), - ]; - - // Insertion des qcm dans la bdd - for (var qcm in testQCMs) { - await qcmRepo.insert(qcm); - await questionRepo.insert(qcm.question!); - - for (var reponse in qcm.reponses!) { - await reponseRepo.insert(reponse); - } - } - - - - - - - //ajout d'un autre cours - - cours = Cours( - idModule: moduleId, - titre: 'Genres journalistiques', - contenu: 'Découvrir les genres journalistiques.'); - - final coursId2 = await coursRepository.create(cours); - - // Ajout des Objectifs du Cours - final objectif4 = ObjectifCours( - idCours: coursId2, - description: 'Les genres d\'information', - ); - final objectif5 = ObjectifCours( - idCours: coursId2, - description: 'Les genres d\'opinion', - ); - - - await objectifCoursRepository.create(objectif4); - await objectifCoursRepository.create(objectif5); - - // Page 4 : Les differents genres d'information - Page page4 = Page(idCours: coursId2, ordre: 1, description: "Qu'elle sont les genres d'information ?",urlAudio: 'lib/data/AppData/Module1/Cours1/genre_d_information.mp3'); - int pageId4 = await pageRepository.create(page4); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId4, - ordre: 1, - url: 'lib/data/AppData/Module1/Cours1/genre_d_information.txt', - type: 'text')); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId4, - ordre: 2, - url: 'lib/data/AppData/Module1/Cours1/genre_d_information.jpg', - type: 'image', - caption: 'Les genres d\'information')); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId4, - ordre : 3, - url : 'lib/data/AppData/Module1/Cours1/genre_d_information2.txt', - type: 'text', - )); - - // Page 5 : Les différents genres d'opinion - Page page5 = Page(idCours: coursId2, ordre: 2, description: "Les genres d'opinion"); - int pageId5 = await pageRepository.create(page5); - - await mediaCoursRepository.create(MediaCours( - idPage: pageId5, - ordre: 1, - url: 'lib/data/AppData/Module1/Cours1/genre_opinion.mp4', - type: 'video')); - - - + final String response = + await rootBundle.loadString('lib/data/AppData/Module1/metadata.json'); + final moduleData = await json.decode(response); - - - // Ajout des autres cours - - - cours = Cours( - idModule: moduleId, - titre: 'Réseaux sociaux', - contenu: ''); - await coursRepository.create(cours); - - cours = Cours( - idModule: moduleId, - titre: 'Désinformation/Mésinformation', - contenu: ''); - await coursRepository.create(cours); -} - -Future insertModule2() async { - // Création du Module final module = Module( - titre: 'Thématique 2', - urlImg: 'lib/data/AppData/facto-societe.png', - description: 'Grâce aux technologies modernes, tout le monde est aujourd’hui en mesure de diffuser des informations et de produire des contenus. Mais tout le monde n’a pas appris les codes, règles et enjeux d’une information responsable à destination du grand public. Que vous ayez 1 à 1 million de followers, ce module est fait pour vous !'); - final moduleId = await moduleRepository.create(module); - - // Création des Cours - Cours cours = Cours( - idModule: moduleId, - titre: 'Ethique professionnelle et personnelle', - contenu: ''); + titre: moduleData['titre'], + urlImg: moduleData['urlImg'] ?? 'assets/facto-societe.png', + description: moduleData['description']); + await moduleRepository.create(module); + + // Cours cours = Cours( + // idModule: moduleId, + // titre: 'Les sources d\'informations', + // contenu: 'Comprendre et évaluer les sources d\'information.', + // description: 'Description des sources d\'informations.'); // final coursId = await coursRepository.create(cours); + // + // // Page 1 : Introduction aux sources d'information + // Page page1 = Page( + // idCours: coursId, + // description: "Qu'est-ce qu'une source d'information ?", + // medias: [ + // MediaItem( + // ordre: 1, + // url: 'lib/data/AppData/Module1/Cours1/source_information_definition.txt', + // type: 'text', + // ), + // MediaItem( + // ordre: 2, + // url: 'lib/data/AppData/Module1/Cours1/journaliste_interview.jpg', + // type: 'image', + // caption: 'Journaliste réalisant une interview', + // ), + // ], + // ); + // await pageRepository.create(page1); + // + // // Page 2 : Les différentes sources + // Page page2 = Page( + // idCours: coursId, + // description: "Types de sources d'information", + // medias: [ + // MediaItem( + // ordre: 1, + // url: 'lib/data/AppData/Module1/Cours1/types_sources.txt', + // type: 'text', + // ), + // MediaItem( + // ordre: 2, + // url: 'lib/data/AppData/Module1/Cours1/source_primaire_secondaire.png', + // type: 'image', + // caption: 'Illustration des sources primaires et secondaires', + // ), + // ], + // ); + // await pageRepository.create(page2); + // + // // Page 3 : Évaluer la crédibilité d'une source + // Page page3 = Page( + // idCours: coursId, + // description: "Comment vérifier la fiabilité d'une source ?", + // medias: [ + // MediaItem( + // ordre: 1, + // url: 'lib/data/AppData/Module1/Cours1/evaluer_sources.txt', + // type: 'text', + // ), + // MediaItem( + // ordre: 2, + // url: 'lib/data/AppData/Module1/Cours1/fake_news_verification.png', + // type: 'image', + // caption: 'Techniques de vérification des fake news', + // ), + // ], + // ); + // await pageRepository.create(page3); + // + // await _insertQCMForCours1(coursId); + // await _insertClozeCours1(coursId); + // + // Cours cours2 = Cours( + // idModule: moduleId, + // titre: 'COURS 2', + // contenu: 'Comprendre le cours 2', + // description: 'Description du cours 2 oui oui oui.'); + // final coursId2 = await coursRepository.create(cours2); + // + // Page page11 = Page( + // idCours: coursId2, + // description: "Description 1", + // medias: [ + // MediaItem( + // ordre: 1, + // url: 'lib/data/AppData/Module1/Cours2/genre_d_information.txt', + // type: 'text', + // ), + // MediaItem( + // ordre: 2, + // url: 'lib/data/AppData/Module1/Cours2/genre_d_information.jpg', + // type: 'image', + // caption: 'Journaliste réalisant une interview', + // ), + // ], + // ); + // await pageRepository.create(page11); + // + // Page page22 = Page( + // idCours: coursId2, + // description: "Description 2", + // medias: [ + // MediaItem( + // ordre: 1, + // url: 'lib/data/AppData/Module1/Cours2/genre_d_information.txt', + // type: 'text', + // ), + // MediaItem( + // ordre: 2, + // url: 'lib/data/AppData/Module1/Cours2/genre_d_information.mp3', + // type: 'audio', + // caption: 'Illustration des sources primaires et secondaires', + // ), + // ], + // ); + // await pageRepository.create(page22); + // + // Page page33 = Page( + // idCours: coursId2, + // description: "Comment vérifier la fiabilité d'une source ?", + // medias: [ + // MediaItem( + // ordre: 1, + // url: 'lib/data/AppData/Module1/Cours2/genre_d_information.txt', + // type: 'text', + // ), + // MediaItem( + // ordre: 2, + // url: 'lib/data/AppData/Module1/Cours2/genre_opinion.mp4', + // type: 'video', + // caption: 'Techniques de vérification des fake news', + // ), + // ], + // ); + // await pageRepository.create(page33); +} - cours = Cours( - idModule: moduleId, - titre: 'Journalisme et production de contenus', - contenu: ''); - await coursRepository.create(cours); +// Future _insertClozeCours1(int coursId) async { +// final repo = ClozeRepository(); +// +// await repo.insert( +// ClozeQuestion( +// phrase: "Le principal indicateur de la fiabilité d'une source d'information est la __________ des informations par d'autres sources fiables.", +// rep1: "popularité sur les réseaux sociaux", +// rep2: "vérifiabilité", +// rep3: "nombre de commentaires sous l\'article", +// rep4: "design du site web", +// soluce: 2, +// idCours: coursId), +// ); +// +// await repo.insert( +// ClozeQuestion( +// phrase: "La meilleure manière de vérifier une information trouvée en ligne est de __________ plusieurs sources fiables et vérifier la cohérence de l'information.", +// rep1: "la partager immédiatement avec ses amis", +// rep2: "consulter", +// rep3: "faire confiance à la première source trouvée", +// rep4: "vérifier si l\'information est amusante avant de la croire", +// soluce: 2, +// idCours: coursId), +// ); +// +// await repo.insert( +// ClozeQuestion( +// phrase: "Un signe révélateur d'une fausse information est qu'elle __________ un ton sensationnaliste et manque de sources vérifiables.", +// rep1: "provient d\'un média reconnu et sérieux", +// rep2: "utilise", +// rep3: "cite plusieurs experts et références", +// rep4: "est reprise par plusieurs médias de confiance", +// soluce: 2, +// idCours: coursId), +// ); +// } +// +// Future _insertQCMForCours1(int coursId) async { +// final db = await DatabaseHelper.instance.database; +// +// // QCM 1 +// await db.insert('qcm', { +// 'question': "Quel est le principal indicateur de la fiabilité d'une source d'information ?", +// 'rep1': 'Sa popularité sur les réseaux sociaux', +// 'rep2': 'La vérifiabilité des informations par d\'autres sources fiables', +// 'rep3': 'Le nombre de commentaires sous l\'article', +// 'rep4': 'Le design du site web', +// 'soluce': 2, +// 'id_cours': coursId, +// }); +// +// // QCM 2 +// await db.insert('qcm', { +// 'question': 'Quelle est la meilleure manière de vérifier une information trouvée en ligne ?', +// 'rep1': 'La partager immédiatement avec ses amis', +// 'rep2': 'Consulter plusieurs sources fiables et vérifier la cohérence de l\'information', +// 'rep3': 'Faire confiance à la première source trouvée', +// 'rep4': 'Vérifier si l\'information est amusante avant de la croire', +// 'soluce': 2, +// 'id_cours': coursId, +// }); +// +// // QCM 3 +// await db.insert('qcm', { +// 'question': 'Quel est un signe révélateur d\'une fausse information ?', +// 'rep1': 'Elle provient d\'un média reconnu et sérieux', +// 'rep2': 'Elle utilise un ton sensationnaliste et manque de sources vérifiables', +// 'rep3': 'Elle cite plusieurs experts et références', +// 'rep4': 'Elle est reprise par plusieurs médias de confiance', +// 'soluce': 2, +// 'id_cours': coursId, +// }); +// } - cours = Cours( - idModule: moduleId, - titre: 'Risques économiques et sociétaux', - contenu: ''); - await coursRepository.create(cours); +Future insertModule2() async { + final String response = + await rootBundle.loadString('lib/data/AppData/Module2/metadata.json'); + final moduleData = await json.decode(response); - cours = Cours( - idModule: moduleId, - titre: 'EMI - Education aux médias et à l’information', - contenu: ''); - await coursRepository.create(cours); + final module = Module( + titre: moduleData['titre'], + urlImg: moduleData['urlImg'] ?? 'assets/facto-societe.png', + description: moduleData['description']); + await moduleRepository.create(module); } Future insertModule3() async { - // Création du Module - final module = Module( - titre: 'Thématique 3', - urlImg: 'lib/data/AppData/facto-societe.png', - description: 'Les journalistes sont des professionnels de l’information. Pourtant, face à la profusion des sources et, parfois aussi, à l’urgence des situations, ils ne maîtrisent pas tous les clés d’une information traitée éthiquement, professionnellement et de manière responsable. Pourquoi ne pas profiter de ce module pour réviser ses classiques, voire en apprendre davantage sur les techniques de vérification les plus performantes ?' - ); - final moduleId = await moduleRepository.create(module); - - // Création des Cours - Cours cours = Cours( - idModule: moduleId, - titre: 'Déontologie', - contenu: ''); - // final coursId = await coursRepository.create(cours); - - cours = Cours( - idModule: moduleId, - titre: 'Osint et Investigation numérique', - contenu: ''); - await coursRepository.create(cours); - - cours = Cours( - idModule: moduleId, - titre: 'SR et fact-checking', - contenu: ''); - await coursRepository.create(cours); - - cours = Cours( - idModule: moduleId, - titre: 'EMI - Education aux médias et à l’information', - contenu: ''); - await coursRepository.create(cours); + final String response = + await rootBundle.loadString('lib/data/AppData/Module3/metadata.json'); + final moduleData = await json.decode(response); -} - -Future insertModule4() async { - // Création du Module final module = Module( - titre: 'Pour aller plus loin', - urlImg: 'lib/data/AppData/facto-societe.png', - description: 'Toutes les références et ressources en relation avec l\'Éducation aux médias et à l’information sont répertoriées ici. ' - ); - final moduleId = await moduleRepository.create(module); - - // Création des Cours - Cours cours = Cours( - idModule: moduleId, - titre: 'Références bibliographiques', - contenu: ''); - // final coursId = await coursRepository.create(cours); - - cours = Cours( - idModule: moduleId, - titre: 'Ressources en ligne', - contenu: ''); - await coursRepository.create(cours); + titre: moduleData['titre'], + urlImg: moduleData['urlImg'] ?? 'assets/facto-societe.png', + description: moduleData['description']); + await moduleRepository.create(module); } Future insertSampleData() async { - await DatabaseHelper.instance.resetDatabase(); - - insertModule1(); - insertModule2(); - insertModule3(); - insertModule4(); + final hasData = await _checkIfDatabaseHasData(); - // Init du singleton CoursSelectionne - CoursSelectionne coursSelectionne = CoursSelectionne.instance; - List lstCours = await coursRepository.getAll(); - coursSelectionne.setCours(lstCours[0]); - - // Init du singleton ModuleSelectionne - ModuleSelectionne moduleSelectionne = ModuleSelectionne.instance; - List lstModule = await moduleRepository.getAll(); - moduleSelectionne.moduleSelectionne = lstModule[0]; - - /* - // Création de Mots (Mots pour le MotsCroises) - final mot1 = Mot( - idMotsCroises: 1, - mot: 'journalisme', - indice: 'Domaine d’étude', - direction: 'horizontal', - positionDepartX: 0, - positionDepartY: 0); - final mot2 = Mot( - idMotsCroises: 1, - mot: 'presse', - indice: 'Média écrit', - direction: 'vertical', - positionDepartX: 1, - positionDepartY: 1); - await motRepository.create(mot1); - await motRepository.create(mot2); - - // Création de MotsCroises - final motsCroises = MotsCroises(idMiniJeu: 1, tailleGrille: '10x10'); - final motsCroisesId = await motsCroisesRepository.create(motsCroises); - - // Création d'un MiniJeu - final miniJeu = MiniJeu( - idCours: coursId, - nom: 'Jeu de mots croisés', - description: 'Mini-jeu de mots croisés sur le journalisme', - progression: 0); - final miniJeuId = await miniJeuRepository.create(miniJeu); - */ - - if (kDebugMode) { - print('Toutes les données d\'exemple ont été insérées avec succès.'); - } - //testRepositories(); -} - -Future testRepositories() async { - final moduleRepository = ModuleRepository(); - final coursRepository = CoursRepository(); - final miniJeuRepository = MiniJeuRepository(); - final mediaCoursRepository = MediaCoursRepository(); - final pageRepository = PageRepository(); - final objectifCoursRepository = ObjectifCoursRepository(); - - // --- Test Objectif --- - if (kDebugMode) { - print('--- Test Objectif ---'); - } - - // Récupérer tous les objectifs - final allObjectifs = await objectifCoursRepository.getAll(); - if (kDebugMode) { - print('Objectifs disponibles : ${allObjectifs.map((e) => e.description).toList()}'); - } - - // Récupérer un objectif par ID - final objectif = allObjectifs.first; - final fetchedObjectif = await objectifCoursRepository.getById(objectif.id!); - if (kDebugMode) { - print('Objectif récupéré par ID : ${fetchedObjectif?.description}'); - } - - // Supprimer un objectif - await objectifCoursRepository.delete(objectif.id!); - if (kDebugMode) { - print('Objectif supprimé.'); - } - - // --- Test MiniJeu --- - if (kDebugMode) { - print('--- Test MiniJeu ---'); - } - - // Récupérer tous les mini-jeux - final allMiniJeux = await miniJeuRepository.getAll(); - if (kDebugMode) { - print('Mini-jeux disponibles : ${allMiniJeux.map((e) => e.nom).toList()}'); - } - - // Récupérer un mini-jeu par ID - final miniJeu = allMiniJeux.first; - final fetchedMiniJeu = await miniJeuRepository.getById(miniJeu.id!); - if (kDebugMode) { - print('Mini-jeu récupéré par ID : ${fetchedMiniJeu?.nom}'); - } - - // Supprimer un mini-jeu - await miniJeuRepository.delete(miniJeu.id!); - if (kDebugMode) { - print('Mini-jeu supprimé.'); - } - - // --- Test MediaCours --- - if (kDebugMode) { - print('--- Test MediaCours ---'); - } - - // Récupérer tous les médias - final allMedias = await mediaCoursRepository.getAll(); - if (kDebugMode) { - print('Médias disponibles : ${allMedias.map((e) => e.url).toList()}'); - } - - // Récupérer un média par ID - final media = allMedias.first; - final fetchedMedia = await mediaCoursRepository.getById(media.id!); - if (kDebugMode) { - print('Média récupéré par ID : ${fetchedMedia?.url}'); - } - - // Supprimer un média - //await mediaCoursRepository.delete(media.id!); - //print('Média supprimé.'); - - // --- Test Page --- - if (kDebugMode) { - print('--- Test Page ---'); - } - -// Récupérer toutes les pages - final allPages = await pageRepository.getAll(); - if (kDebugMode) { - print('Pages disponibles : ${allPages.map((e) => e.id).toList()}'); - } - -// Récupérer une page par ID - if (allPages.isNotEmpty) { - final page = allPages.first; - final fetchedPage = await pageRepository.getById(page.id!); + if (!hasData) { if (kDebugMode) { - print('Page récupérée par ID : ${fetchedPage?.id} liée au cours : ${fetchedPage?.idCours}'); + print('Première installation - Création des données de base'); } - - // Supprimer une page - //await pageRepository.delete(page.id!); - //print('Page supprimée.'); + await insertModule1(); + await insertModule2(); + await insertModule3(); } else { if (kDebugMode) { - print('Aucune page disponible pour le test.'); + print('Base de données existante chargée'); } } - // --- Test Cours --- - if (kDebugMode) { - print('--- Test Cours ---'); - } - - // Récupérer toutes les courss - final allCours = await coursRepository.getAll(); - if (kDebugMode) { - print('Cours disponibles : ${allCours.map((e) => e.titre).toList()}'); + CoursSelectionne coursSelectionne = CoursSelectionne.instance; + List lstCours = await coursRepository.getAll(); + if (lstCours.isNotEmpty) { + coursSelectionne.setCours(lstCours[0]); } - // Récupérer une cours par ID - final cours = allCours.first; - final fetchedCours = await coursRepository.getById(cours.id!); if (kDebugMode) { - print('Cours récupérée par ID : ${fetchedCours?.titre}'); + print(coursSelectionne); } - // méthode loadContenu(Cours cours) - cours.pages = await pageRepository.getPagesByCourseId(cours.id!); - if (kDebugMode) { - print("Nombre de page récupéré : ${cours.pages?.length}"); + // Init du singleton ModuleSelectionne + ModuleSelectionne moduleSelectionne = ModuleSelectionne.instance; + List lstModule = await moduleRepository.getAll(); + if (lstModule.isNotEmpty) { + moduleSelectionne.moduleSelectionne = lstModule[0]; } +} - for (int i=0; i _checkIfDatabaseHasData() async { + try { + final cours = await coursRepository.getAll(); + if (cours.isNotEmpty) { + return true; } - // Supprimer une cours - await coursRepository.delete(cours.id!); - if (kDebugMode) { - print('Cours supprimée.'); - } - // --- Test Module --- - if (kDebugMode) { - print('--- Test Module ---'); - } + final modules = await moduleRepository.getAll(); + if (modules.isNotEmpty) { + return true; + } - // Récupérer tous les module - final allModulees = await moduleRepository.getAll(); - if (kDebugMode) { - print('Module disponibles : ${allModulees.map((e) => e.titre).toList()}'); + return false; + } catch (e) { + if (kDebugMode) { + print('Erreur lors de la vérification de la BDD: $e'); + } + return false; } +} - // Récupérer un module par ID - final module = allModulees.first; - final fetchedModule = await moduleRepository.getById(module.id!); - if (kDebugMode) { - print('Module récupéré par ID : ${fetchedModule?.titre}'); - } +Future resetDatabaseForDebug() async { + await DatabaseHelper.instance.resetDB(); + await insertModule1(); + await insertModule2(); + await insertModule3(); - // Supprimer un module - await moduleRepository.delete(module.id!); if (kDebugMode) { - print('Module supprimé.'); + print('Base de données réinitialisée'); } -} +} \ No newline at end of file diff --git a/lib/database_helper.dart b/lib/database_helper.dart new file mode 100644 index 0000000..d7697d3 --- /dev/null +++ b/lib/database_helper.dart @@ -0,0 +1,110 @@ +import 'package:sqflite/sqflite.dart'; +import 'package:path/path.dart'; + +class DatabaseHelper { + static final DatabaseHelper instance = DatabaseHelper._init(); + static Database? _database; + + DatabaseHelper._init(); + + Future get database async { + if (_database != null) return _database!; + _database = await _initDB('app.db'); + return _database!; + } + + Future _initDB(String filePath) async { + final dbPath = await getDatabasesPath(); + final path = join(dbPath, filePath); + return await openDatabase( + path, + version: 1, + onOpen: (db) async { + await _createDB(db, 1); + }, + ); + } + + Future _createDB(Database db, int version) async { + // Table Module + await db.execute(''' + CREATE TABLE IF NOT EXISTS module ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + titre TEXT NOT NULL, + description TEXT NOT NULL, + urlImg TEXT + ); + '''); + + // Table Cours + await db.execute(''' + CREATE TABLE IF NOT EXISTS cours ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + titre TEXT NOT NULL, + description TEXT NOT NULL, + contenu TEXT NOT NULL, + id_module INTEGER, + last_updated TEXT, + is_downloaded INTEGER DEFAULT 0, + FOREIGN KEY (id_module) REFERENCES module (id) ON DELETE CASCADE + ); + '''); + + // Table Page avec medias en JSON + await db.execute(''' + CREATE TABLE IF NOT EXISTS page ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + description TEXT, + contenu TEXT, + medias TEXT, + est_vue INTEGER DEFAULT 0, + id_cours INTEGER NOT NULL, + FOREIGN KEY (id_cours) REFERENCES cours (id) ON DELETE CASCADE + ); + '''); + + // Table QCM + await db.execute(''' + CREATE TABLE IF NOT EXISTS qcm ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question TEXT NOT NULL, + rep1 TEXT NOT NULL, + rep2 TEXT NOT NULL, + rep3 TEXT NOT NULL, + rep4 TEXT NOT NULL, + soluce INTEGER NOT NULL, + id_cours INTEGER NOT NULL, + FOREIGN KEY (id_cours) REFERENCES cours (id) ON DELETE CASCADE + ); + '''); + + await db.execute(''' + CREATE TABLE IF NOT EXISTS Cloze ( + idCloze INTEGER PRIMARY KEY AUTOINCREMENT, + phrase TEXT NOT NULL, + idCours INTEGER NOT NULL, + rep1 TEXT NOT NULL, + rep2 TEXT NOT NULL, + rep3 TEXT NOT NULL, + rep4 TEXT NOT NULL, + soluce INTEGER NOT NULL, + FOREIGN KEY(idCours) REFERENCES cours(id) + ); +'''); + + } + + Future resetDB() async { + final dbPath = await getDatabasesPath(); + final path = join(dbPath, 'app.db'); + await deleteDatabase(path); + _database = await _initDB('app.db'); + } + + Future close() async { + final db = _database; + if (db != null) { + await db.close(); + } + } +} \ No newline at end of file diff --git a/lib/logic/ProgressionUseCase.dart b/lib/logic/progression_use_case.dart similarity index 79% rename from lib/logic/ProgressionUseCase.dart rename to lib/logic/progression_use_case.dart index 08cadbd..edbaa82 100644 --- a/lib/logic/ProgressionUseCase.dart +++ b/lib/logic/progression_use_case.dart @@ -1,12 +1,16 @@ +import 'package:factoscope/repositories/cours_repository.dart'; +import 'package:factoscope/repositories/module_repository.dart'; +import 'package:factoscope/repositories/page_repository.dart'; import 'package:flutter/foundation.dart'; -import 'package:seriouse_game/repositories/coursRepository.dart'; -import 'package:seriouse_game/repositories/moduleRepository.dart'; -import 'package:seriouse_game/repositories/pageRepository.dart'; + +import '../models/cours.dart'; +import '../repositories/Cloze/cloze_repository.dart'; class ProgressionUseCase { final pageRepository = PageRepository(); final coursRepository = CoursRepository(); - final moduleRepository = ModuleRepository(); + final moduleRepository = ModuleRepository(); + final ClozeRepository _repository = ClozeRepository(); ProgressionUseCase(); @@ -28,7 +32,8 @@ class ProgressionUseCase { return pourcentage; } catch (e) { if (kDebugMode) { - print("Erreur lors du calcul du pourcentage de progression globale : $e"); + print( + "Erreur lors du calcul du pourcentage de progression globale : $e"); } return 0; } @@ -52,7 +57,8 @@ class ProgressionUseCase { return pourcentage; } catch (e) { if (kDebugMode) { - print("Erreur lors du calcul du pourcentage de progression de module : $e"); + print( + "Erreur lors du calcul du pourcentage de progression de module : $e"); } return 0; } @@ -69,7 +75,7 @@ class ProgressionUseCase { // Calcul du pourcentage de pages vues double pourcentage; - if (totalPages>0) { + if (totalPages > 0) { pourcentage = (pagesVues / totalPages) * 100; } else { pourcentage = 0; @@ -84,6 +90,12 @@ class ProgressionUseCase { } } + Future getNombrePageDeCloze(Cours cours) async { + final clozes = + await _repository.getByCoursId(cours.id!); + return clozes.length; + } + // Méthode pour récupérer le pourcentage de pages vues pour un cours donné en passant l'ID du cours en paramètre Future calculerProgressionActuelleCours(int coursId, int page) async { try { diff --git a/lib/main.dart b/lib/main.dart index cd344a7..3ea3f74 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,41 +1,92 @@ import 'package:flutter/material.dart'; -import 'package:seriouse_game/service_locator.dart'; +import 'package:factoscope/service_locator.dart'; import 'data_initializer.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'ui/App.dart'; +import 'ui/app.dart'; -void main() { +Future main() async { + await dotenv.load(fileName: ".env"); setupLocator(); runApp(const MainApp()); - //runApp(MyApp()); } class MainApp extends StatelessWidget { const MainApp({super.key}); @override - Widget build(BuildContext context) { - // FutureBuilder permet d'attendre que les données d'exemple soient insérés avant le lancement de l'UI + Widget build(BuildContext context) { return FutureBuilder( - future: insertSampleData(), // Insertion des données dans la bdd - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.done: // L'insertion est fini : - return MaterialApp.router( // Voir App.dart pour avoir le routeur et le 1er widget de l'app - //debugShowCheckedModeBanner: false, - routerConfig: router, - ); - - default: // L'insertion n'a pas fini : Page d'attente #TODO - return const CircularProgressIndicator(); - } - - } - ); + // future: insertSampleData(), + future: resetDatabaseForDebug(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return MaterialApp.router( + routerConfig: router, + debugShowCheckedModeBanner: false, + ); + } else { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + backgroundColor: Colors.white, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.school, + size: 80, + color: Color.fromARGB(255, 236, 187, 139), + ), + const SizedBox(height: 24), + const Text( + 'Factoscope', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 40), + const SizedBox( + width: 40, + height: 40, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Color.fromARGB(255, 236, 187, 139), + ), + ), + ), + const SizedBox(height: 16), + const Text( + 'Chargement...', + style: TextStyle( + fontSize: 14, + color: Colors.black54, + ), + ), + // Message d'erreur si problème + if (snapshot.hasError) + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'Erreur: ${snapshot.error}', + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ); + } + }); } } - class MyApp extends StatefulWidget { const MyApp({super.key}); @@ -62,16 +113,12 @@ class _MyAppState extends State { title: 'Flutter Demo', home: Scaffold( appBar: AppBar( - title: const Text("Courses List"), ), body: const Center( child: Text("Hello"), ), - ), ); } } - - diff --git a/lib/main_diagnostic.dart b/lib/main_diagnostic.dart new file mode 100644 index 0000000..e165cd7 --- /dev/null +++ b/lib/main_diagnostic.dart @@ -0,0 +1,216 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:factoscope/service_locator.dart'; +import 'data_initializer.dart'; +import 'ui/app.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + if (kDebugMode) { + print('🔵 main() appelé'); + } + + setupLocator(); + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + if (kDebugMode) { + print('🔵 MainApp.build() appelé'); + } + + return const MaterialApp( + debugShowCheckedModeBanner: false, + home: SplashScreen(), + ); + } +} + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + bool _isLoading = true; + String _statusMessage = 'Initialisation...'; + + @override + void initState() { + super.initState(); + if (kDebugMode) { + print('🔵 SplashScreen.initState() appelé'); + } + _initialize(); + } + + Future _initialize() async { + try { + if (kDebugMode) { + print('🔵 Début de l\'initialisation'); + } + + setState(() { + _statusMessage = 'Chargement des données...'; + }); + + await insertSampleData(); + + if (kDebugMode) { + print('🔵 insertSampleData() terminé'); + } + + setState(() { + _statusMessage = 'Préparation de l\'interface...'; + }); + + // Petit délai pour s'assurer que tout est prêt + await Future.delayed(const Duration(milliseconds: 300)); + + if (kDebugMode) { + print('🔵 Navigation vers l\'app principale'); + } + + if (mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => const ActualApp(), + ), + ); + } + } catch (e) { + if (kDebugMode) { + print('🔴 ERREUR lors de l\'initialisation: $e'); + } + setState(() { + _statusMessage = 'Erreur: $e'; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + if (kDebugMode) { + print('🔵 SplashScreen.build() appelé'); + } + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: const Color.fromARGB(255, 236, 187, 139), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.school, + size: 60, + color: Colors.white, + ), + ), + const SizedBox(height: 32), + + // Titre + const Text( + 'Factoscope', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + + // Sous-titre + const Text( + 'Éducation aux médias et à l\'information', + style: TextStyle( + fontSize: 14, + color: Colors.black54, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + + // Indicateur de chargement + if (_isLoading) + const SizedBox( + width: 40, + height: 40, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Color.fromARGB(255, 236, 187, 139), + ), + ), + ), + + const SizedBox(height: 16), + + // Message de statut + Text( + _statusMessage, + style: const TextStyle( + fontSize: 14, + color: Colors.black54, + ), + textAlign: TextAlign.center, + ), + + // Bouton de retry en cas d'erreur + if (!_isLoading) + Padding( + padding: const EdgeInsets.only(top: 24), + child: ElevatedButton( + onPressed: () { + setState(() { + _isLoading = true; + _statusMessage = 'Nouvelle tentative...'; + }); + _initialize(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 236, 187, 139), + foregroundColor: Colors.white, + ), + child: const Text('Réessayer'), + ), + ), + ], + ), + ), + ), + ); + } +} + +class ActualApp extends StatelessWidget { + const ActualApp({super.key}); + + @override + Widget build(BuildContext context) { + if (kDebugMode) { + print('🔵 ActualApp.build() appelé'); + } + + return MaterialApp.router( + routerConfig: router, + debugShowCheckedModeBanner: false, + ); + } +} \ No newline at end of file diff --git a/lib/models/Cloze/cloze_page.dart b/lib/models/Cloze/cloze_page.dart new file mode 100644 index 0000000..ec005fb --- /dev/null +++ b/lib/models/Cloze/cloze_page.dart @@ -0,0 +1,25 @@ +class ClozeQuestion { + final int? id; + final String phrase; + final String rep1; + final String rep2; + final String rep3; + final String rep4; + final int soluce; + final int idCours; + + ClozeQuestion({this.id, required this.phrase, required this.rep1, required this.rep2, required this.rep3, required this.rep4, required this.soluce, required this.idCours}); + + factory ClozeQuestion.fromMap(Map map) { + return ClozeQuestion( + id: map['idCloze'], + phrase: map['phrase'], + rep1: map['rep1'], + rep2: map['rep2'], + rep3: map['rep3'], + rep4: map['rep4'], + soluce: map['soluce'], + idCours: map['idCours'], + ); + } +} \ No newline at end of file diff --git a/lib/models/ListCoursViewModel.dart b/lib/models/ListCoursViewModel.dart deleted file mode 100644 index 10f71a4..0000000 --- a/lib/models/ListCoursViewModel.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:seriouse_game/logic/ProgressionUseCase.dart'; -import 'package:seriouse_game/models/cours.dart'; -import 'package:seriouse_game/models/module.dart'; -import 'package:seriouse_game/repositories/coursRepository.dart'; -import 'package:seriouse_game/ui/ModuleSelectionne.dart'; - -//Classe permettant d'extraire les cours d'un module -class ListCoursViewModel with ChangeNotifier { - - final progressionUseCase = ProgressionUseCase(); - - //Méthode pour changer la liste coursDuModule du Singleton ModuleSelectionne par celle correspondant à la liste des cours du module d'id idModule - Future recupererCours(int? idModule) async { - - CoursRepository repository = CoursRepository(); - - //On accède à la liste des cours de la base de donnée par la méthode de CoursRepository - ModuleSelectionne().updateListModule(await repository.getCoursesByModuleId(idModule!)); - - //la liste change donc on avertit les listeners - notifyListeners(); - } - - Future getProgressionModule(Module module) async { - return await progressionUseCase.calculerProgressionCours(module.id!)/100; - } - - Future getProgressionCours(Cours cours) async { - return await progressionUseCase.calculerProgressionCours(cours.id!)/100; - } - - -} \ No newline at end of file diff --git a/lib/models/ListModuleViewModel.dart b/lib/models/ListModuleViewModel.dart deleted file mode 100644 index 66d839d..0000000 --- a/lib/models/ListModuleViewModel.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:seriouse_game/logic/ProgressionUseCase.dart'; -import 'package:seriouse_game/models/module.dart'; -import 'package:seriouse_game/repositories/moduleRepository.dart'; - -//Classe permettant d'extraire la liste des modules -class ListModuleViewModel with ChangeNotifier { - - final progressionUseCase = ProgressionUseCase(); - - List listModule = List.empty(); - - //Méthode pour changer la liste listmodule par celle correspondant à la liste de tous les modules de l'application - Future recupererModule() async { - - //Création d'un objet repository pour accéder aux modules de l'application - ModuleRepository repository = ModuleRepository(); - - //On accède à la liste des module de la base de donnée par la méthode de ModuleRepository - listModule = await repository.getAll(); - - //la liste change donc on avertit les listeners - notifyListeners(); - } - - Future getProgressionGlobale() async { - double progress = await progressionUseCase.calculerProgressionGlobale(); - return progress.round(); - } - -} \ No newline at end of file diff --git a/lib/models/QCM/qcm.dart b/lib/models/QCM/qcm.dart index 7c10d1a..3026a27 100644 --- a/lib/models/QCM/qcm.dart +++ b/lib/models/QCM/qcm.dart @@ -1,42 +1,52 @@ - -import 'question.dart'; -import 'reponse.dart'; - -/// Modèle représentant un QCM (Questionnaire à Choix Multiples). class QCM { - final int id; - final int numSolution; - final int idCours; - final int idQuestion; - Question? question; - List? reponses; + int? id; + String question; + String rep1; + String rep2; + String rep3; + String rep4; + int soluce; + int idCours; QCM({ - required this.id, - required this.numSolution, + this.id, + required this.question, + required this.rep1, + required this.rep2, + required this.rep3, + required this.rep4, + required this.soluce, required this.idCours, - required this.idQuestion, - this.question, - this.reponses, }); - /// Crée une instance de QCM à partir d'une map. + Map toMap() { + return { + 'id': id, + 'question': question, + 'rep1': rep1, + 'rep2': rep2, + 'rep3': rep3, + 'rep4': rep4, + 'soluce': soluce, + 'id_cours': idCours, + }; + } + factory QCM.fromMap(Map map) { return QCM( - id: map['idQCM'], - numSolution: map['numSolution'], - idCours: map['idCours'], - idQuestion: map['idQuestion'], + id: map['id'], + question: map['question'], + rep1: map['rep1'], + rep2: map['rep2'], + rep3: map['rep3'], + rep4: map['rep4'], + soluce: map['soluce'], + idCours: map['id_cours'], ); } - /// Convertit l'objet QCM en map pour la base de données. - Map toMap() { - return { - 'idQCM': id, - 'numSolution': numSolution, - 'idCours': idCours, - 'idQuestion': idQuestion, - }; + // Helper pour obtenir les réponses sous forme de liste + List getReponses() { + return [rep1, rep2, rep3, rep4]; } -} \ No newline at end of file +} diff --git a/lib/models/QCM/reponse.dart b/lib/models/QCM/reponse.dart index 57f1efd..424c302 100644 --- a/lib/models/QCM/reponse.dart +++ b/lib/models/QCM/reponse.dart @@ -7,7 +7,13 @@ class Reponse { final String? caption; final String type; - Reponse({required this.id, required this.idQCM, this.text, this.imageUrl, this.caption, required this.type}); + Reponse( + {required this.id, + required this.idQCM, + this.text, + this.imageUrl, + this.caption, + required this.type}); /// Crée une instance de Reponse à partir d'une map. factory Reponse.fromMap(Map map) { @@ -17,7 +23,11 @@ class Reponse { text: map['txt'], imageUrl: map['urlImage'], caption: map['caption'], - type: map.containsKey('txt') ? 'text' : map.containsKey('urlImage') ? 'image' : 'unknown', + type: map.containsKey('txt') + ? 'text' + : map.containsKey('urlImage') + ? 'image' + : 'unknown', ); } diff --git a/lib/models/cours.dart b/lib/models/cours.dart index 153dee9..54106bc 100644 --- a/lib/models/cours.dart +++ b/lib/models/cours.dart @@ -1,39 +1,57 @@ -import 'package:seriouse_game/models/objectifCours.dart'; -import 'package:seriouse_game/models/page.dart'; +import 'package:factoscope/models/page.dart'; class Cours { - int? id; - int idModule; - String titre; - String contenu; - List? pages; - List? objectifs; + int? id; + int? idModule; + String titre; + String contenu; + String description; + int isDownloaded; + List? pages; + Cours({ this.id, - required this.idModule, + this.idModule, required this.titre, required this.contenu, + required this.description, + this.isDownloaded = 0, + this.pages, }); - // Convertir une Cours en Map pour SQLite Map toMap() { return { 'id': id, - 'id_Module': idModule, + 'id_module': idModule, 'titre': titre, 'contenu': contenu, + 'description': description, + 'is_downloaded': isDownloaded, }; } - // Construire une Cours depuis un Map (SQLite) factory Cours.fromMap(Map map) { return Cours( id: map['id'], - idModule: map['id_module'], + idModule: map['id_module'] as int?, titre: map['titre'], contenu: map['contenu'], + description: map['description'], + isDownloaded: map['is_downloaded'] ?? 0, ); } - -} + factory Cours.fromJson(Map json) { + return Cours( + id: json['id'], + idModule: json['id_module'], + titre: json['titre'], + contenu: json['contenu'] ?? json['description'], + description: json['description'], + isDownloaded: 1, + pages: json['pages'] != null + ? (json['pages'] as List).map((page) => Page.fromJson(page)).toList() + : null, + ); + } +} \ No newline at end of file diff --git a/lib/models/list_cours_view_model.dart b/lib/models/list_cours_view_model.dart new file mode 100644 index 0000000..6d447fa --- /dev/null +++ b/lib/models/list_cours_view_model.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:factoscope/logic/progression_use_case.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/models/module.dart'; +import 'package:factoscope/repositories/cours_repository.dart'; +import 'package:factoscope/ui/module_selectionne.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +import '../config.dart'; + +class ListCoursViewModel with ChangeNotifier { + final progressionUseCase = ProgressionUseCase(); + final CoursRepository _coursRepository = CoursRepository(); + bool _isLoading = false; + List _cours = []; + + bool get isLoading => _isLoading; + List get cours => _cours; + + Future getCours(int? idModule) async { + if (idModule == null) return; + + _isLoading = true; + notifyListeners(); + + try { + _cours = await _coursRepository.getCoursesByModuleId(idModule); + ModuleSelectionne().updateListModule(_cours); + } catch (e) { + debugPrint("Erreur lors de la récupération des cours : $e"); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Future getAllCours() async { + _isLoading = true; + notifyListeners(); + + try { + _cours = await _coursRepository.getAll(); + } catch (e) { + debugPrint("Erreur lors de la récupération de tous les cours : $e"); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Future downloadCours(int coursId) async { + _isLoading = true; + notifyListeners(); + + try { + final response = await http + .get(Uri.parse('${AppConfig.effectiveApiUrl}/api/cours/$coursId')); + if (response.statusCode == 200) { + final courseData = jsonDecode(response.body); + + // Créer ou mettre à jour le cours avec ses pages et médias en JSON + await _coursRepository.createOrUpdate(Cours.fromJson(courseData)); + + // Marquer le cours comme téléchargé + await _coursRepository.markAsDownloaded(coursId); + + await getCours(ModuleSelectionne().moduleSelectionne.id); + } + } catch (e) { + debugPrint("Erreur lors du téléchargement du cours : $e"); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // Calcule la progression d'un module + Future getProgressionModule(Module module) async { + return await progressionUseCase.calculerProgressionCours(module.id!) / 100; + } + + // Calcule la progression d'un cours + Future getProgressionCours(Cours cours) async { + return await progressionUseCase.calculerProgressionCours(cours.id!) / 100; + } +} \ No newline at end of file diff --git a/lib/models/list_module_view_model.dart b/lib/models/list_module_view_model.dart new file mode 100644 index 0000000..6972b1a --- /dev/null +++ b/lib/models/list_module_view_model.dart @@ -0,0 +1,31 @@ +import 'package:flutter/foundation.dart'; +import 'package:factoscope/logic/progression_use_case.dart'; +import 'package:factoscope/models/module.dart'; +import 'package:factoscope/repositories/module_repository.dart'; + +// Classe permettant d'extraire la liste des modules +class ListModuleViewModel with ChangeNotifier { + final progressionUseCase = ProgressionUseCase(); + + List listModule = List.empty(); + + Future recupererModule() async { + try { + listModule = await ModuleRepository().getAll(); + notifyListeners(); + } catch (e) { + if (kDebugMode) { + print("Erreur lors de la récupération des modules : $e"); + } + } + } + + Future getProgressionGlobale() async { + double progress = await progressionUseCase.calculerProgressionGlobale(); + return progress.round(); + } + + Future getProgressionModule(Module module) async { + return 0.0; + } +} diff --git a/lib/models/mediaCours.dart b/lib/models/mediaCours.dart deleted file mode 100644 index a42e613..0000000 --- a/lib/models/mediaCours.dart +++ /dev/null @@ -1,41 +0,0 @@ -class MediaCours { - int? id; - int idPage; - int ordre; - String url; - String type; - String? caption; - - MediaCours({ - this.id, - required this.idPage, - required this.ordre, - required this.url, - required this.type, - this.caption, - }); - - // Conversion en Map pour SQLite - Map toMap() { - return { - 'id': id, - 'id_page': idPage, - 'ordre': ordre, - 'url': url, - 'type': type, - 'caption': caption, - }; - } - - // Conversion d'une Map SQLite en objet MediaCours - factory MediaCours.fromMap(Map map) { - return MediaCours( - id: map['id'], - idPage: map['id_page'], - ordre: map['ordre'], - url: map['url'], - type: map['type'], - caption: map['caption'], - ); - } -} diff --git a/lib/models/minijeu.dart b/lib/models/mini_jeu.dart similarity index 88% rename from lib/models/minijeu.dart rename to lib/models/mini_jeu.dart index f6a7415..c70cad0 100644 --- a/lib/models/minijeu.dart +++ b/lib/models/mini_jeu.dart @@ -1,9 +1,9 @@ class MiniJeu { - int? id; - int idCours; - String nom; - String? description; - int progression; + int? id; + int idCours; + String nom; + String? description; + int progression; MiniJeu({ this.id, @@ -34,6 +34,4 @@ class MiniJeu { progression: map['progression'], ); } - - } diff --git a/lib/models/module.dart b/lib/models/module.dart index ef37cc8..0450c87 100644 --- a/lib/models/module.dart +++ b/lib/models/module.dart @@ -1,12 +1,12 @@ -import 'package:seriouse_game/models/cours.dart'; - +import 'package:factoscope/models/cours.dart'; class Module { - int? id; // `id` est nullable pour les nouvelles entrées - String titre; - String urlImg; - String description; - List? cours; + int? id; + String titre; + String urlImg; + String description; + List? cours; + Module({ this.id, required this.urlImg, @@ -14,17 +14,15 @@ class Module { required this.description, }); - // Convertir un objet en Map pour SQLite Map toMap() { return { 'id': id, - 'urlImg':urlImg, + 'urlImg': urlImg, 'titre': titre, 'description': description, }; } - // Convertir une ligne SQLite en objet Module static Module fromMap(Map map) { return Module( id: map['id'], @@ -33,5 +31,4 @@ class Module { description: map['description'], ); } - } diff --git a/lib/models/objectifCours.dart b/lib/models/objectifCours.dart deleted file mode 100644 index d565202..0000000 --- a/lib/models/objectifCours.dart +++ /dev/null @@ -1,25 +0,0 @@ -class ObjectifCours { - int? id; - int idCours; - String description; - - ObjectifCours({this.id, required this.idCours, required this.description}); - - // Convertir en Map pour SQLite - Map toMap() { - return { - 'id': id, - 'id_cours': idCours, - 'description': description, - }; - } - - // Conversion Map vers l'objet ObjectifCours - factory ObjectifCours.fromMap(Map map) { - return ObjectifCours( - id: map['id'], - idCours: map['id_cours'], - description: map['description'], - ); - } -} diff --git a/lib/models/page.dart b/lib/models/page.dart index 61db3b6..314a406 100644 --- a/lib/models/page.dart +++ b/lib/models/page.dart @@ -1,44 +1,117 @@ -import 'package:seriouse_game/models/mediaCours.dart'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; class Page { int? id; - int ordre; + String? description; + String? contenu; int idCours; - String urlAudio; int estVue; - String? description ; - List? medias; + List? medias; Page({ this.id, - required this.ordre, - this.urlAudio = "", - this.estVue=0, + this.description, + this.contenu, required this.idCours, - this.description , + this.estVue = 0, + this.medias, }); - // Convertir un objet Page en Map pour SQLite Map toMap() { return { 'id': id, - 'ordre': ordre, + 'description': description, + 'contenu': contenu, 'id_cours': idCours, - 'description':description, - 'est_vue':estVue, - 'urlAudio':urlAudio, + 'est_vue': estVue, + 'medias': medias != null ? jsonEncode(medias!.map((m) => m.toJson()).toList()) : null, }; } - // Créer un objet Page à partir d'une Map SQLite factory Page.fromMap(Map map) { + List? mediaList; + if (map['medias'] != null && map['medias'] != '') { + try { + final decoded = jsonDecode(map['medias'] as String) as List; + mediaList = decoded.map((m) => MediaItem.fromJson(m)).toList(); + } catch (e) { + if (kDebugMode) { + print("Erreur parsing medias JSON: $e"); + } + mediaList = null; + } + } + return Page( id: map['id'], - ordre: map['ordre'], - idCours: map['id_cours'], description: map['description'], - estVue: map['est_vue'], - urlAudio: map['urlAudio'] + contenu: map['contenu'], + idCours: map['id_cours'], + estVue: map['est_vue'] ?? 0, + medias: mediaList, + ); + } + + factory Page.fromJson(Map json) { + List? mediaList; + if (json['medias'] != null) { + if (json['medias'] is String) { + // Si c'est une string JSON, on la décode + try { + final decoded = jsonDecode(json['medias']) as List; + mediaList = decoded.map((m) => MediaItem.fromJson(m)).toList(); + } catch (e) { + if (kDebugMode) { + print("Erreur parsing medias JSON string: $e"); + } + } + } else if (json['medias'] is List) { + // Si c'est déjà une liste + mediaList = (json['medias'] as List).map((m) => MediaItem.fromJson(m)).toList(); + } + } + + return Page( + id: json['id'], + description: json['description'], + contenu: json['content'], + idCours: json['id_cours'], + estVue: json['est_vue'] ?? 0, + medias: mediaList, + ); + } +} + +class MediaItem { + int ordre; + String url; + String type; + String? caption; + + MediaItem({ + required this.ordre, + required this.url, + required this.type, + this.caption, + }); + + Map toJson() { + return { + 'ordre': ordre, + 'url': url, + 'type': type, + if (caption != null) 'caption': caption, + }; + } + + factory MediaItem.fromJson(Map json) { + return MediaItem( + ordre: json['ordre'] ?? 0, + url: json['url'] ?? '', + type: json['type'] ?? 'text', + caption: json['caption'], ); } } \ No newline at end of file diff --git a/lib/repositories/Cloze/cloze_repository.dart b/lib/repositories/Cloze/cloze_repository.dart new file mode 100644 index 0000000..a121b96 --- /dev/null +++ b/lib/repositories/Cloze/cloze_repository.dart @@ -0,0 +1,31 @@ +import '../../database_helper.dart'; +import '../../models/Cloze/cloze_page.dart'; + +class ClozeRepository { + final DatabaseHelper _dbHelper = DatabaseHelper.instance; + + Future> getByCoursId(int coursId) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'Cloze', + where: 'idCours = ?', + whereArgs: [coursId], + ); + + return List.generate(maps.length, (i) { + return ClozeQuestion.fromMap(maps[i]); + }); + } + Future insert(ClozeQuestion question) async { + final db = await _dbHelper.database; + return await db.insert('Cloze', { + 'phrase': question.phrase, + 'rep1': question.rep1, + 'rep2': question.rep2, + 'rep3': question.rep3, + 'rep4': question.rep4, + 'soluce': question.soluce, + 'idCours': question.idCours, + }); + } +} \ No newline at end of file diff --git a/lib/repositories/QCM/QCMRepository.dart b/lib/repositories/QCM/QCMRepository.dart deleted file mode 100644 index 5586cd7..0000000 --- a/lib/repositories/QCM/QCMRepository.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:seriouse_game/models/QCM/qcm.dart'; - -import 'package:seriouse_game/repositories/QCM/QuestionRepository.dart'; -import 'package:seriouse_game/repositories/QCM/ReponseRepository.dart'; -import 'package:seriouse_game/DataBase/database_helper.dart'; - -/// Repository pour gérer les opérations CRUD des QCM. -class QCMRepository { - /// Insère un nouveau QCM dans la base de données. - Future insert(QCM qcm) async { - final db = await DatabaseHelper.instance.database; - return await db.insert('QCM', qcm.toMap()); - } - - /// Récupère tous les QCM. - Future> getAll() async { - final db = await DatabaseHelper.instance.database; - final List> maps = await db.query('QCM'); - return maps.map((map) => QCM.fromMap(map)).toList(); - } - - /// Récupère un QCM par son identifiant et complète ses listes de questions et réponses. - Future getById(int id) async { - final db = await DatabaseHelper.instance.database; - final maps = await db.query('QCM', where: 'idQCM = ?', whereArgs: [id]); - if (maps.isNotEmpty) { - QCM qcm = QCM.fromMap(maps.first); - - final questionRepo = QuestionRepository(); - qcm.question = await questionRepo.getById(qcm.idQuestion); - - final reponseRepo = ReponseRepository(); - qcm.reponses = await reponseRepo.getByQCMId(qcm.id); - - return qcm; - } - return null; - } - - /// Récupère tous les QCM. - Future> getAllIdByCoursId(int idCours) async { - final db = await DatabaseHelper.instance.database; - final List> maps = await db.query('QCM', where: "idCours = ?", whereArgs: [idCours]); - - List qcmIds = []; - for (final qcmMap in maps) { - qcmIds.add(qcmMap["idQCM"] as int); - } - return qcmIds; - } - -} \ No newline at end of file diff --git a/lib/repositories/QCM/fin_cours_view.dart b/lib/repositories/QCM/fin_cours_view.dart new file mode 100644 index 0000000..7b93d41 --- /dev/null +++ b/lib/repositories/QCM/fin_cours_view.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:go_router/go_router.dart'; + +class FinCoursView extends StatelessWidget { + final Cours cours; + + const FinCoursView({ + super.key, + required this.cours, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 40), + + // Icône de succès + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.celebration, + size: 100, + color: Colors.green, + ), + ), + const SizedBox(height: 32), + + // Titre + const Text( + "Félicitations !", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // Message + Text( + "Vous avez terminé le cours\n« ${cours.titre} »", + style: const TextStyle( + fontSize: 20, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + + // Message d'encouragement + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 90, 230, 220) + .withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + const Icon( + Icons.emoji_events, + size: 50, + color: Color.fromRGBO(252, 179, 48, 1), + ), + const SizedBox(height: 16), + const Text( + "Excellent travail !", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + "Vous avez acquis de nouvelles connaissances importantes sur ${cours.titre.toLowerCase()}.", + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ), + ], + ), + ), + const SizedBox(height: 40), + + // Bouton retour à la liste des cours + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + GoRouter.of(context).go('/cours'); + }, + icon: const Icon(Icons.arrow_back, color: Colors.white), + label: const Text( + "Retour à la liste des cours", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 20, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + backgroundColor: const Color.fromRGBO(252, 179, 48, 1), + ), + ), + ), + + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/repositories/QCM/qcm_controller.dart b/lib/repositories/QCM/qcm_controller.dart new file mode 100644 index 0000000..7f33d08 --- /dev/null +++ b/lib/repositories/QCM/qcm_controller.dart @@ -0,0 +1,80 @@ +import '../../models/QCM/qcm.dart'; + +enum QCMState { notStarted, inProgress, finished } + +class QCMController { + final List qcmList; + + int _currentIndex = 0; + List _selectedAnswers = []; + QCMState _state = QCMState.notStarted; + + bool? isCorrect; // null = pas encore répondu + + QCMController(this.qcmList) { + _selectedAnswers = List.filled(qcmList.length, null); + } + + // --- GETTERS --- + int get currentIndex => _currentIndex; + QCM get currentQuestion => qcmList[_currentIndex]; + QCMState get state => _state; + int? get selectedAnswer => _selectedAnswers[_currentIndex]; + bool get isLastQuestion => _currentIndex == qcmList.length - 1; + + // --- ACTIONS --- + void start() { + _state = QCMState.inProgress; + _currentIndex = 0; + isCorrect = null; + } + + /// L'utilisateur clique sur une réponse + void selectAnswer(int index) { + _selectedAnswers[_currentIndex] = index; + + // Vérifie si c'est correct + isCorrect = (index == currentQuestion.soluce); + } + + bool canGoNext() => selectedAnswer != null; + bool canGoPrevious() => _currentIndex > 0; + + /// Passe à la question suivante + void next() { + if (!canGoNext()) return; + + if (isLastQuestion) { + _state = QCMState.finished; + } else { + _currentIndex++; + isCorrect = null; // reset pour la prochaine question + } + } + + void previous() { + if (canGoPrevious()) { + _currentIndex--; + isCorrect = null; + } + } + + /// Calcul du score final + int getScore() { + int score = 0; + for (int i = 0; i < qcmList.length; i++) { + if (_selectedAnswers[i] == qcmList[i].soluce) { + score++; + } + } + return score; + } + + /// Redémarre le QCM + void restart() { + _currentIndex = 0; + _selectedAnswers = List.filled(qcmList.length, null); + _state = QCMState.notStarted; + isCorrect = null; + } +} diff --git a/lib/repositories/QCM/qcm_repository.dart b/lib/repositories/QCM/qcm_repository.dart new file mode 100644 index 0000000..7e7e4f4 --- /dev/null +++ b/lib/repositories/QCM/qcm_repository.dart @@ -0,0 +1,69 @@ +import 'package:factoscope/database_helper.dart'; +import 'package:factoscope/models/QCM/qcm.dart'; + +class QCMRepository { + final DatabaseHelper _dbHelper = DatabaseHelper.instance; + + Future insert(QCM qcm) async { + final db = await _dbHelper.database; + return await db.insert('qcm', qcm.toMap()); + } + + Future getById(int id) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'qcm', + where: 'id = ?', + whereArgs: [id], + ); + + if (maps.isNotEmpty) { + return QCM.fromMap(maps.first); + } + return null; + } + + Future> getAllByCoursId(int coursId) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'qcm', + where: 'id_cours = ?', + whereArgs: [coursId], + ); + + return List.generate(maps.length, (i) { + return QCM.fromMap(maps[i]); + }); + } + + Future> getAllIdByCoursId(int coursId) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'qcm', + columns: ['id'], + where: 'id_cours = ?', + whereArgs: [coursId], + ); + + return maps.map((map) => map['id'] as int).toList(); + } + + Future update(QCM qcm) async { + final db = await _dbHelper.database; + return await db.update( + 'qcm', + qcm.toMap(), + where: 'id = ?', + whereArgs: [qcm.id], + ); + } + + Future delete(int id) async { + final db = await _dbHelper.database; + return await db.delete( + 'qcm', + where: 'id = ?', + whereArgs: [id], + ); + } +} diff --git a/lib/repositories/QCM/QuestionRepository.dart b/lib/repositories/QCM/question_repository.dart similarity index 66% rename from lib/repositories/QCM/QuestionRepository.dart rename to lib/repositories/QCM/question_repository.dart index f3c69de..de1eb98 100644 --- a/lib/repositories/QCM/QuestionRepository.dart +++ b/lib/repositories/QCM/question_repository.dart @@ -1,6 +1,6 @@ -import 'package:seriouse_game/models/QCM/question.dart'; +import 'package:factoscope/models/QCM/question.dart'; -import 'package:seriouse_game/DataBase/database_helper.dart'; +import 'package:factoscope/database_helper.dart'; /// Repository pour gérer les opérations CRUD des Questions. class QuestionRepository { @@ -8,14 +8,21 @@ class QuestionRepository { Future insert(Question question) async { final db = await DatabaseHelper.instance.database; int id = await db.insert('Question', {'idQuestion': question.id}); - + // Vérifie si la question est de type texte et insère les données correspondantes. if (question.type == 'text' && question.text != null) { - await db.insert('QuestionText', {'idQuestion': question.id, 'txt': question.text}); - } + await db.insert( + 'QuestionText', {'idQuestion': question.id, 'txt': question.text}); + } // Vérifie si la question est de type image et insère les données correspondantes. - else if (question.type == 'image' && question.imageUrl != null && question.caption != null) { - await db.insert('QuestionImg', {'idQuestion': question.id, 'urlImage': question.imageUrl, 'caption': question.caption}); + else if (question.type == 'image' && + question.imageUrl != null && + question.caption != null) { + await db.insert('QuestionImg', { + 'idQuestion': question.id, + 'urlImage': question.imageUrl, + 'caption': question.caption + }); } return id; } @@ -30,22 +37,24 @@ class QuestionRepository { /// Récupère une question par son identifiant et détermine son type. Future getById(int id) async { final db = await DatabaseHelper.instance.database; - List> textResult = await db.query('QuestionText', where: 'idQuestion = ?', whereArgs: [id]); - + List> textResult = await db + .query('QuestionText', where: 'idQuestion = ?', whereArgs: [id]); + // Si une entrée existe dans QuestionText, la question est de type texte. if (textResult.isNotEmpty) { Question a = Question.fromMap(textResult.first); a.type = "text"; return a; } - - final imgResult = await db.query('QuestionImg', where: 'idQuestion = ?', whereArgs: [id]); - + + final imgResult = + await db.query('QuestionImg', where: 'idQuestion = ?', whereArgs: [id]); + // Si une entrée existe dans QuestionImg, la question est de type image. if (imgResult.isNotEmpty) { return Question.fromMap(imgResult.first..['type'] = 'image'); } - + return null; } -} \ No newline at end of file +} diff --git a/lib/repositories/QCM/ReponseRepository.dart b/lib/repositories/QCM/reponse_repository.dart similarity index 67% rename from lib/repositories/QCM/ReponseRepository.dart rename to lib/repositories/QCM/reponse_repository.dart index f05ba3f..484b873 100644 --- a/lib/repositories/QCM/ReponseRepository.dart +++ b/lib/repositories/QCM/reponse_repository.dart @@ -1,15 +1,18 @@ -import 'package:seriouse_game/models/QCM/reponse.dart'; +import 'package:factoscope/models/QCM/reponse.dart'; -import 'package:seriouse_game/DataBase/database_helper.dart'; +import 'package:factoscope/database_helper.dart'; /// Repository pour gérer les opérations CRUD des Réponses. class ReponseRepository { Future> getByQCMId(int qcmId) async { final db = await DatabaseHelper.instance.database; - final List> responseMaps; - responseMaps = await db.query('Reponse', - where: 'idQCM = ?', whereArgs: [qcmId], - orderBy: "idReponse",); // Le order est nécessaire pour l'attribut numSolution du QCM + final List> responseMaps; + responseMaps = await db.query( + 'Reponse', + where: 'idQCM = ?', + whereArgs: [qcmId], + orderBy: "idReponse", + ); // Le order est nécessaire pour l'attribut numSolution du QCM List responses = []; @@ -57,16 +60,24 @@ class ReponseRepository { /// Insère une réponse dans la base de données. Future insert(Reponse reponse) async { final db = await DatabaseHelper.instance.database; - int id = await db.insert('Reponse', {'idReponse': reponse.id, 'idQCM': reponse.idQCM}); - + int id = await db + .insert('Reponse', {'idReponse': reponse.id, 'idQCM': reponse.idQCM}); + // Vérifie si la réponse est de type texte et insère les données correspondantes. if (reponse.type == 'text' && reponse.text != null) { - await db.insert('ReponseText', {'idReponse': reponse.id, 'txt': reponse.text}); - } + await db.insert( + 'ReponseText', {'idReponse': reponse.id, 'txt': reponse.text}); + } // Vérifie si la réponse est de type image et insère les données correspondantes. - else if (reponse.type == 'image' && reponse.imageUrl != null && reponse.caption != null) { - await db.insert('ReponseImg', {'idReponse': reponse.id, 'urlImage': reponse.imageUrl, 'caption': reponse.caption}); + else if (reponse.type == 'image' && + reponse.imageUrl != null && + reponse.caption != null) { + await db.insert('ReponseImg', { + 'idReponse': reponse.id, + 'urlImage': reponse.imageUrl, + 'caption': reponse.caption + }); } return id; } -} \ No newline at end of file +} diff --git a/lib/repositories/QCM/transition_qcm_view.dart b/lib/repositories/QCM/transition_qcm_view.dart new file mode 100644 index 0000000..11c42f9 --- /dev/null +++ b/lib/repositories/QCM/transition_qcm_view.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/ui/Cours/cours_view_model.dart'; + +class TransitionQCMView extends StatelessWidget { + final Cours cours; + final CoursViewModel coursViewModel; + + const TransitionQCMView({ + super.key, + required this.cours, + required this.coursViewModel, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Icône + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color.fromRGBO(252, 179, 48, 1) + .withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.quiz, + size: 80, + color: Color.fromRGBO(252, 179, 48, 1), + ), + ), + const SizedBox(height: 32), + + // Titre + const Text( + "Temps de vérifier vos connaissances !", + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + // Règles du jeu + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "📋 Règles du jeu", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + _buildRule("✓", "Lisez attentivement chaque question"), + _buildRule( + "✓", "Sélectionnez la réponse qui vous semble correcte"), + _buildRule( + "✓", "Validez pour voir si votre réponse est bonne"), + _buildRule("✓", "Vous verrez immédiatement le résultat"), + ], + ), + ), + const SizedBox(height: 32), + + // Message d'encouragement + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 90, 230, 220) + .withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color.fromARGB(255, 90, 230, 220), + width: 2, + ), + ), + child: Row( + children: [ + const Icon( + Icons.lightbulb, + color: Color.fromARGB(255, 3, 47, 122), + size: 30, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + "Prenez votre temps et réfléchissez bien. Bonne chance !", + style: TextStyle( + fontSize: 16, + color: Colors.grey[800], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 40), + + // Bouton pour commencer + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + coursViewModel.changementPageSuivante(cours); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 20, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + backgroundColor: const Color.fromRGBO(252, 179, 48, 1), + ), + child: const Text( + "Commencer le jeu", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildRule(String icon, String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + icon, + style: const TextStyle( + fontSize: 18, + color: Color.fromARGB(255, 90, 230, 220), + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: const TextStyle(fontSize: 16), + ), + ), + ], + ), + ); + } +} diff --git a/lib/repositories/coursRepository.dart b/lib/repositories/cours_repository.dart similarity index 61% rename from lib/repositories/coursRepository.dart rename to lib/repositories/cours_repository.dart index c5e9be0..c43e698 100644 --- a/lib/repositories/coursRepository.dart +++ b/lib/repositories/cours_repository.dart @@ -1,55 +1,60 @@ -import '../DataBase/database_helper.dart'; + +import '../database_helper.dart'; import '../models/cours.dart'; class CoursRepository { final DatabaseHelper _dbHelper = DatabaseHelper.instance; - // Créer une Cours Future create(Cours cours) async { final db = await _dbHelper.database; - return await db.insert('Cours', cours.toMap()); + return await db.insert('cours', cours.toMap()); + } + + Future createOrUpdate(Cours cours) async { + final existingCours = await getById(cours.id!); + if (existingCours != null) { + return await update(cours); + } else { + return await create(cours); + } } - // Lire toutes les Courss Future> getAll() async { final db = await _dbHelper.database; - final List> maps = await db.query('Cours'); - + final List> maps = await db.query('cours'); return List.generate(maps.length, (i) { return Cours.fromMap(maps[i]); }); } - // Lire une Cours par son ID Future getById(int id) async { final db = await _dbHelper.database; final List> maps = await db.query( - 'Cours', + 'cours', where: 'id = ?', whereArgs: [id], ); if (maps.isNotEmpty) { - return Cours.fromMap(maps.first); + final cours = Cours.fromMap(maps.first); + return cours; } return null; } - // Mettre à jour une Cours Future update(Cours cours) async { final db = await _dbHelper.database; return await db.update( - 'Cours', + 'cours', cours.toMap(), where: 'id = ?', whereArgs: [cours.id], ); } - // Supprimer une Cours Future delete(int id) async { final db = await _dbHelper.database; return await db.delete( - 'Cours', + 'cours', where: 'id = ?', whereArgs: [id], ); @@ -58,11 +63,20 @@ class CoursRepository { Future> getCoursesByModuleId(int moduleId) async { final db = await _dbHelper.database; final result = await db.query( - 'Cours', - where: 'id_Module = ?', + 'cours', + where: 'id_module = ?', whereArgs: [moduleId], ); return result.map((map) => Cours.fromMap(map)).toList(); } -} + Future markAsDownloaded(int coursId) async { + final db = await _dbHelper.database; + return await db.update( + 'cours', + {'is_downloaded': 1}, + where: 'id = ?', + whereArgs: [coursId], + ); + } +} \ No newline at end of file diff --git a/lib/repositories/mediaCoursRepository.dart b/lib/repositories/mediaCoursRepository.dart deleted file mode 100644 index b2ce0c5..0000000 --- a/lib/repositories/mediaCoursRepository.dart +++ /dev/null @@ -1,56 +0,0 @@ -import '../DataBase/database_helper.dart'; -import '../models/mediaCours.dart'; - -class MediaCoursRepository { - final DatabaseHelper _dbHelper = DatabaseHelper.instance; - - Future create(MediaCours mediaCours) async { - final db = await _dbHelper.database; - return await db.insert('MediaCours', mediaCours.toMap()); - } - - Future getById(int id) async { - final db = await _dbHelper.database; - final result = await db.query( - 'MediaCours', - where: 'id = ?', - whereArgs: [id], - ); - return result.isNotEmpty ? MediaCours.fromMap(result.first) : null; - } - - Future> getAll() async { - final db = await _dbHelper.database; - final result = await db.query('MediaCours'); - return result.map((map) => MediaCours.fromMap(map)).toList(); - } - - Future update(MediaCours mediaCours) async { - final db = await _dbHelper.database; - return await db.update( - 'MediaCours', - mediaCours.toMap(), - where: 'id = ?', - whereArgs: [mediaCours.id], - ); - } - - Future delete(int id) async { - final db = await _dbHelper.database; - return await db.delete( - 'MediaCours', - where: 'id = ?', - whereArgs: [id], - ); - } - - Future> getByPageId(int pageId) async { - final db = await _dbHelper.database; - final result = await db.query( - 'MediaCours', - where: 'id_page = ?', - whereArgs: [pageId], - ); - return result.map((map) => MediaCours.fromMap(map)).toList(); - } -} diff --git a/lib/repositories/minijeuRepository.dart b/lib/repositories/minijeu_repository.dart similarity index 95% rename from lib/repositories/minijeuRepository.dart rename to lib/repositories/minijeu_repository.dart index a025d1e..b8f319c 100644 --- a/lib/repositories/minijeuRepository.dart +++ b/lib/repositories/minijeu_repository.dart @@ -1,5 +1,5 @@ -import '../DataBase/database_helper.dart'; -import '../models/minijeu.dart'; +import '../database_helper.dart'; +import '../models/mini_jeu.dart'; class MiniJeuRepository { final DatabaseHelper _dbHelper = DatabaseHelper.instance; @@ -66,5 +66,4 @@ class MiniJeuRepository { return result.isNotEmpty ? MiniJeu.fromMap(result.first) : null; } - } diff --git a/lib/repositories/moduleRepository.dart b/lib/repositories/module_repository.dart similarity index 67% rename from lib/repositories/moduleRepository.dart rename to lib/repositories/module_repository.dart index f8c849f..f7e8949 100644 --- a/lib/repositories/moduleRepository.dart +++ b/lib/repositories/module_repository.dart @@ -1,13 +1,13 @@ -import 'package:seriouse_game/models/module.dart'; -import 'package:seriouse_game/DataBase/database_helper.dart'; +import 'package:factoscope/models/module.dart'; +import 'package:factoscope/database_helper.dart'; -class ModuleRepository{ +class ModuleRepository { final DatabaseHelper _dbHelper = DatabaseHelper.instance; // --- CRUD Methods --- - Future create(Module Module) async { + Future create(Module module) async { final db = await _dbHelper.database; - return await db.insert('module', Module.toMap()); + return await db.insert('module', module.toMap()); } Future getById(int id) async { @@ -24,23 +24,23 @@ class ModuleRepository{ return null; } - Future> getAll() async { + Future> getAll() async { final db = await _dbHelper.database; final result = await db.query('module'); return result.map((map) => Module.fromMap(map)).toList(); } - Future update(Module Module) async { + Future update(Module module) async { final db = await _dbHelper.database; return await db.update( 'module', - Module.toMap(), + module.toMap(), where: 'id = ?', - whereArgs: [Module.id], + whereArgs: [module.id], ); } - Future delete(int id) async { + Future delete(int id) async { final db = await _dbHelper.database; return await db.delete( 'module', @@ -48,5 +48,4 @@ class ModuleRepository{ whereArgs: [id], ); } - -} \ No newline at end of file +} diff --git a/lib/repositories/objectifCoursRepository.dart b/lib/repositories/objectifCoursRepository.dart deleted file mode 100644 index cfd037f..0000000 --- a/lib/repositories/objectifCoursRepository.dart +++ /dev/null @@ -1,57 +0,0 @@ - -import '../DataBase/database_helper.dart'; -import '../models/objectifCours.dart'; - -class ObjectifCoursRepository { - final DatabaseHelper _dbHelper = DatabaseHelper.instance; - - Future create(ObjectifCours objectifCours) async { - final db = await _dbHelper.database; - return await db.insert('ObjectifCours', objectifCours.toMap()); - } - - Future getById(int id) async { - final db = await _dbHelper.database; - final result = await db.query( - 'ObjectifCours', - where: 'id = ?', - whereArgs: [id], - ); - return result.isNotEmpty ? ObjectifCours.fromMap(result.first) : null; - } - - Future> getAll() async { - final db = await _dbHelper.database; - final result = await db.query('ObjectifCours'); - return result.map((map) => ObjectifCours.fromMap(map)).toList(); - } - - Future> getByCoursId(int coursId) async { - final db = await _dbHelper.database; - final result = await db.query( - 'ObjectifCours', - where: 'id_cours = ?', - whereArgs: [coursId], - ); - return result.map((map) => ObjectifCours.fromMap(map)).toList(); - } - - Future update(ObjectifCours objectifCours) async { - final db = await _dbHelper.database; - return await db.update( - 'ObjectifCours', - objectifCours.toMap(), - where: 'id = ?', - whereArgs: [objectifCours.id], - ); - } - - Future delete(int id) async { - final db = await _dbHelper.database; - return await db.delete( - 'ObjectifCours', - where: 'id = ?', - whereArgs: [id], - ); - } -} diff --git a/lib/repositories/pageRepository.dart b/lib/repositories/page_repository.dart similarity index 68% rename from lib/repositories/pageRepository.dart rename to lib/repositories/page_repository.dart index 65f5c51..d65334f 100644 --- a/lib/repositories/pageRepository.dart +++ b/lib/repositories/page_repository.dart @@ -1,5 +1,4 @@ -import 'package:flutter/foundation.dart'; -import 'package:seriouse_game/DataBase/database_helper.dart'; +import 'package:factoscope/database_helper.dart'; import 'package:sqflite/sqflite.dart'; import '../models/page.dart'; @@ -46,45 +45,43 @@ class PageRepository { ); } - Future> getPagesByCourseId(int courseId) async { + Future> getPagesByCourseId(int coursId) async { final db = await _dbHelper.database; - final result = await db.query( + final List> maps = await db.query( 'page', where: 'id_cours = ?', - whereArgs: [courseId], + whereArgs: [coursId], ); - return result.map((map) => Page.fromMap(map)).toList(); + return List.generate(maps.length, (i) { + return Page.fromMap(maps[i]); + }); } -Future getNbPageByCourseId(int courseId) async { - final db = await _dbHelper.database; - final result = await db.rawQuery( - 'SELECT COUNT(*) as count FROM page WHERE id_cours = ?', - [courseId], - ); - return Sqflite.firstIntValue(result) ?? 0; -} + Future getNbPageByCourseId(int courseId) async { + final db = await _dbHelper.database; + final result = await db.rawQuery( + 'SELECT COUNT(*) as count FROM page WHERE id_cours = ?', + [courseId], + ); + return Sqflite.firstIntValue(result) ?? 0; + } - Future getNbPageVisite(int courseId) async { + Future getNbPageVisite(int courseId) async { final db = await _dbHelper.database; final result = await db.rawQuery( 'SELECT COUNT(*) as count FROM page WHERE id_cours = ? AND est_vue = ?', - [courseId, 1], // On compte les pages vues (est_vue = 1) + [courseId, 1], ); return Sqflite.firstIntValue(result) ?? 0; } Future setPageVisite(int pageId) async { final db = await _dbHelper.database; - if (kDebugMode) { - print("Page $pageId visité"); - } return await db.update( 'page', - {'est_vue' : true}, + {'est_vue': 1}, where: 'id = ?', whereArgs: [pageId], ); } - } diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 6982fb7..45c4055 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -1,17 +1,9 @@ import 'package:get_it/get_it.dart'; -import 'package:seriouse_game/repositories/coursRepository.dart'; -import 'package:seriouse_game/repositories/objectifCoursRepository.dart'; -import 'package:seriouse_game/services/coursService.dart'; +import 'package:factoscope/repositories/cours_repository.dart'; final getIt = GetIt.instance; void setupLocator() { // Enregistrer les repositories en tant que singletons paresseux getIt.registerLazySingleton(() => CoursRepository()); - getIt.registerLazySingleton(() => ObjectifCoursRepository()); - - // Enregistrer le CoursService en injectant les dépendances - getIt.registerLazySingleton( - () => CoursService(getIt(), getIt()), - ); -} +} \ No newline at end of file diff --git a/lib/services/cloze_service.dart b/lib/services/cloze_service.dart new file mode 100644 index 0000000..140b78a --- /dev/null +++ b/lib/services/cloze_service.dart @@ -0,0 +1,14 @@ +import '../repositories/Cloze/cloze_repository.dart'; +import '../models/Cloze/cloze_page.dart'; + +class ClozeService { + final ClozeRepository _repository = ClozeRepository(); + + Future> getQuestionsPourCours(int coursId) async { + return await _repository.getByCoursId(coursId); + } + + bool verifierReponse(String saisieUtilisateur, String reponseAttendue) { + return saisieUtilisateur.trim().toLowerCase() == reponseAttendue.trim().toLowerCase(); + } +} \ No newline at end of file diff --git a/lib/services/coursService.dart b/lib/services/coursService.dart deleted file mode 100644 index 3f8030b..0000000 --- a/lib/services/coursService.dart +++ /dev/null @@ -1,26 +0,0 @@ -import '../models/cours.dart'; -import '../repositories/coursRepository.dart'; -import '../repositories/objectifCoursRepository.dart'; - -class CoursService { - final CoursRepository _coursRepository; - final ObjectifCoursRepository _objectifRepository; - - // Injection via le constructeur - CoursService(this._coursRepository, this._objectifRepository); - - // Récupérer un cours et lui ajouter ses objectifs - Future getCoursWithObjectifs(int coursId) async { - // Récupérer le cours par son ID - final cours = await _coursRepository.getById(coursId); - if (cours == null) return null; - - // Récupérer les objectifs associés au cours - final objectifs = await _objectifRepository.getByCoursId(coursId); - - // Ajouter les objectifs au cours - cours.objectifs = objectifs; - - return cours; - } -} diff --git a/lib/ui/App.dart b/lib/ui/App.dart deleted file mode 100644 index c5d5981..0000000 --- a/lib/ui/App.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:seriouse_game/ui/LaunchScreen/LaunchScreenView.dart'; -import 'package:seriouse_game/ui/ListCoursView.dart'; -import 'package:seriouse_game/ui/ListModuleView.dart'; -import 'package:seriouse_game/ui/Cours/CoursView.dart'; - -final _rootNavigatorKey = GlobalKey(); -final _shellNavigatorKey = GlobalKey(); - -final router = GoRouter( - navigatorKey: _rootNavigatorKey, - routes: [ - ShellRoute( - navigatorKey: _shellNavigatorKey, - builder: (context, state, child) => App(child: child), - routes: [ - GoRoute(path: '/', builder: (context, state) => const ListModulesView()), - GoRoute(path: '/module', builder: (context, state) => ListCoursView()), - GoRoute(path: '/cours', builder: (context, state) => CoursView()), - ], - ), - ], -); - -class App extends StatefulWidget { - const App({super.key, this.child}); - - final Widget? child; - - @override - State createState() => - _AppState(); -} -class _AppState extends State { - int currentIndex = 0; - bool showLaunchScreen = true; - - @override - void initState() { - super.initState(); - - Future.delayed(const Duration(milliseconds: 4000), () { - setState(() { - showLaunchScreen = false; - }); - }); - } - - void changeTab(int index) { - switch (index) { - case 0: - context.go('/'); - break; - case 1: - context.go('/module'); - break; - case 2: - default: - context.go('/'); - break; - } - setState(() { - currentIndex = index; - }); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Scaffold( - appBar: AppBar( - backgroundColor: const Color.fromRGBO(252, 179, 48, 1), - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'lib/data/AppData/CharteFactoscope/logo-factoscope_seul_2.png', - height: 40, - width: 190, - fit: BoxFit.contain, - ), - const SizedBox(width: 10), - ], - ), - centerTitle: true, - ), - body: widget.child!, - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Color.fromRGBO(41, 36, 96, 1), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: BottomNavigationBar( - onTap: changeTab, - backgroundColor: Colors.transparent, - currentIndex: currentIndex, - unselectedItemColor: Colors.white, - selectedItemColor: const Color.fromRGBO(252, 179, 48, 1), - items: const [ - BottomNavigationBarItem( - icon: Padding( - padding: EdgeInsets.only(top: 5), - child: Icon(Icons.home), - ), - label: 'Home', - ), - BottomNavigationBarItem( - icon: Icon(Icons.book), - label: 'Modules', - ), - BottomNavigationBarItem( - icon: Icon(Icons.verified), - label: 'Certification', - ), - ], - ), - ), - ), - if (showLaunchScreen) LaunchScreenView(), - ], - ); - } -} - -/* -class App extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - int _selectedIndex = 0; - - final List _pages = [ - Center(child: Text('Page Home')), - Center(child: Text('Page Modules')), - Center(child: Text('Page Certification')), - ]; - - void _onItemTapped(int index) { - setState(() { - _selectedIndex = index; - }); - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - home: Scaffold( - appBar: AppBar( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'lib/data/AppData/facto-logo.png', - height: 40, // Ajuste la hauteur - fit: BoxFit.contain, // Garde les proportions - ), - const SizedBox(width: 10), // Espace entre l'image et le texte - const Text('Factoscope'), - ], - ), - centerTitle: true, - ), - body: _pages[_selectedIndex], - bottomNavigationBar: BottomNavigationBar( - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Home', - ), - BottomNavigationBarItem( - icon: Icon(Icons.book), - label: 'Modules', - ), - BottomNavigationBarItem( - icon: Icon(Icons.verified), - label: 'Certification', - ), - ], - currentIndex: _selectedIndex, - selectedItemColor: const Color.fromRGBO(252, 179, 48, 1), - onTap: _onItemTapped, - ), - ), - ); - } -} - - -*/ \ No newline at end of file diff --git a/lib/ui/Cloze/cloze_page.dart b/lib/ui/Cloze/cloze_page.dart new file mode 100644 index 0000000..a8d257d --- /dev/null +++ b/lib/ui/Cloze/cloze_page.dart @@ -0,0 +1,277 @@ +import '../../services/cloze_service.dart'; +import 'package:flutter/material.dart'; +import '../../models/Cloze/cloze_page.dart'; +import 'cloze_result_page.dart'; + +class ClozePage extends StatefulWidget { + final int coursId; + + const ClozePage({ + super.key, + required this.coursId, + }); + + @override + State createState() => _ClozePageState(); +} + +class _ClozePageState extends State { + final service = ClozeService(); + List questions = []; + String? selectedAnswer; + String feedback = ''; + bool? isCorrect; + int score = 0; + bool isFinished = false; + int currentIndex = 0; + + final double buttonWidth = 140; + final double buttonHeight = 45; + + late final ButtonStyle buttonStyle = ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFcb330), + foregroundColor: const Color(0xFF292466), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ); + + @override + void initState() { + super.initState(); + service.getQuestionsPourCours(widget.coursId).then((value) { + setState(() => questions = value); + }); + } + + Widget _buildAnswer(String answer) { + Color bgColor = Colors.white; + if (selectedAnswer != null && answer == selectedAnswer) { + bgColor = isCorrect! ? Colors.green[200]! : Colors.red[200]!; + } + + return GestureDetector( + onTap: selectedAnswer != null + ? null + : () { + setState(() { + selectedAnswer = answer; + _validerReponse(); + }); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + margin: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + answer, + style: const TextStyle( + fontSize: 16, + color: Colors.black87, + ), + ), + ), + ); + } + + void _validerReponse() { + if (selectedAnswer == null) return; + + final question = questions[currentIndex]; + String soluce; + switch (question.soluce) { + case 1: + soluce = question.rep1; + break; + case 2: + soluce = question.rep2; + break; + case 3: + soluce = question.rep3; + break; + case 4: + soluce = question.rep4; + break; + default: + soluce = ""; + } + + final ok = service.verifierReponse(selectedAnswer!, soluce); + + setState(() { + isCorrect = ok; + feedback = ok ? 'Bonne réponse' : 'Mauvaise réponse'; + if (ok) { + score++; + } + }); + } + + @override + Widget build(BuildContext context) { + if (questions.isEmpty) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + if (isFinished) { + return Scaffold( + appBar: AppBar(title: const Text("Résultat")), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Votre score", + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + Text( + "$score / ${questions.length}", + style: const TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: () { + setState(() { + score = 0; + isFinished = false; + selectedAnswer = null; + isCorrect = null; + }); + }, + child: const Text("Recommencer"), + ), + ], + ), + ), + ); + } + + final question = questions[currentIndex]; + final List propositions = [ + question.rep1, + question.rep2, + question.rep3, + question.rep4, + ]; + + return Scaffold( + appBar: AppBar( + title: const Text('Texte à trous'), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + question.phrase, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + if (isCorrect != null) + Text( + isCorrect! ? "Bonne réponse" : "Mauvaise réponse", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isCorrect! ? Colors.green : Colors.red, + ), + ), + const SizedBox(height: 12), + ...propositions.map(_buildAnswer), + ], + ), + ), + ), + + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: buttonWidth, + height: buttonHeight, + child: ElevatedButton( + style: buttonStyle, + onPressed: currentIndex > 0 + ? () { + setState(() { + currentIndex--; + selectedAnswer = null; + isCorrect = null; + }); + } + : null, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.arrow_back, + size: 14, color: Color(0xFF292466)), + SizedBox(width: 4), + Text('Précédent'), + ], + ), + ), + ), + SizedBox( + width: buttonWidth, + height: buttonHeight, + child: ElevatedButton( + style: buttonStyle, + onPressed: () { + if (currentIndex >= questions.length - 1) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => ClozeResultPage( + score: score, + totalQuestions: questions.length, + coursId: widget.coursId, + ), + ), + ); + + } else { + setState(() { + currentIndex++; + selectedAnswer = null; + isCorrect = null; + }); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + currentIndex >= questions.length - 1 + ? 'Terminer' + : 'Suivant',), + const SizedBox(width: 4), + const Icon(Icons.arrow_forward, + size: 14, color: Color(0xFF292466)), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/Cloze/cloze_result_page.dart b/lib/ui/Cloze/cloze_result_page.dart new file mode 100644 index 0000000..b7a71e0 --- /dev/null +++ b/lib/ui/Cloze/cloze_result_page.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'cloze_page.dart'; + +class ClozeResultPage extends StatelessWidget { + final int score; + final int totalQuestions; + final int coursId; + + const ClozeResultPage({ + super.key, + required this.score, + required this.totalQuestions, + required this.coursId, + + }); + + @override + Widget build(BuildContext context) { + bool isPerfect = score == totalQuestions; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + centerTitle: true, + title: const Text("Résultat"), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + + Icon( + isPerfect ? Icons.celebration : Icons.thumb_up, + size: 80, + color: isPerfect ? Colors.green : Colors.orange, + ), + + const SizedBox(height: 20), + + Text( + isPerfect ? "Félicitations !" : "Bravo !", + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + ), + ), + + const SizedBox(height: 15), + + Text( + isPerfect + ? "Excellent travail ! Vous avez tout réussi 🎉" + : "Bon travail ! Continuez à vous entraîner pour progresser 💪", + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + + const SizedBox(height: 20), + + Text( + "Score : $score / $totalQuestions", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + + const SizedBox(height: 30), + ElevatedButton( + onPressed: () { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => ClozePage( + coursId: coursId, + ), + ), + ); + }, + child: const Text("Recommencer"), + ), + + const SizedBox(height: 12), + + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/Contenu/ContenuCoursView.dart b/lib/ui/Contenu/ContenuCoursView.dart deleted file mode 100644 index 5bc117b..0000000 --- a/lib/ui/Contenu/ContenuCoursView.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:seriouse_game/models/cours.dart'; -import 'package:seriouse_game/ui/Contenu/WidgetContenu/ContenuAudioWidget.dart'; -import 'package:seriouse_game/ui/Contenu/WidgetContenu/ContenuImageWidget.dart'; -import 'package:seriouse_game/ui/Contenu/WidgetContenu/ContenuVideoWidget.dart'; -import 'package:seriouse_game/ui/Contenu/WidgetContenu/ContenuTextWidget.dart'; - -class ContenuCoursView extends StatelessWidget { - final Cours cours; - final int selectedPageIndex; - - const ContenuCoursView({ - super.key, - required this.cours, - required this.selectedPageIndex, - }); - - @override - Widget build(BuildContext context) { - // Vérifier que l'index est valide - if (cours.pages == null || selectedPageIndex < 0 || selectedPageIndex >= cours.pages!.length) { - if (kDebugMode) { - print("Page introuvable"); - } - return const Center(child: Text("Page introuvable")); - } - - var page = cours.pages![selectedPageIndex]; - - List lstWidgetAudio = []; - if (page.urlAudio!="") { - lstWidgetAudio = [ContenuAudioWidget(urlAudio: page.urlAudio).build(context)]; - } - - - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: List.from( - lstWidgetAudio - - )..addAll( - page.medias?.map((media) { - if (kDebugMode) { - print("Media url: ${media.url}"); - } - - if (media.type == "image") { - return Center(child: ContenuImageWidget(media: media)) ; - } else if (media.type == "video") { - return ContenuVideoWidget(data: media); - } else if (media.type == "text") { - return ContenuTextWidget(filePath: media.url); - } else { - return const Text("Le Media n'a pas le bon type !"); // Cas inconnu - } - }).toList() ?? [] - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/ui/Contenu/ContenuCoursViewModel.dart b/lib/ui/Contenu/ContenuCoursViewModel.dart deleted file mode 100644 index 6e5f686..0000000 --- a/lib/ui/Contenu/ContenuCoursViewModel.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:flutter/services.dart'; -import 'package:seriouse_game/models/mediaCours.dart'; -import 'package:video_player/video_player.dart'; - -class ContenuCoursViewModel { - ContenuCoursViewModel(); - - //Méthode permettant d'initialiser un lecteur vidéo avec l'url d'un fichier contenue dans mediaModel (MediaCours) - Future VideoLoader(MediaCours mediaModel) async { - - //On initialise le controller à renvoyer - late VideoPlayerController controller; - - //On teste si le type de média est bien celui voulu - if(mediaModel.type!="video"){ - throw Exception("Wrong type of ressources"); - } - - //On vérifie si le fichier vidéo correspondant à l'url de mediaModel existe - try { - - await rootBundle.load(mediaModel.url); - - } on Exception { - rethrow; - } - - //On initialise et retourne le controller vidéo - controller = VideoPlayerController.asset(mediaModel.url); - - return controller ; - - } - - //Méthode permettant d'initialiser un lecteur audio avec l'url d'un fichier contenue dans un modèle. - Future AudioLoader(String urlAudio) async { - - /* - //Teste si le modèle envoyée est bien un modèle prévu pour un fichier Audio. Sinon on renvoie une erreur. - if(mediaModel.type!="audio"){ - throw Exception("Wrong type of ressources"); - } - */ - - //Création du lecteur audio - final player = AudioPlayer(); - //Par défaut AudioPlayer cherche les fichiers audios dans le dossier assets. - //N'utilisant pas de dossier de ce non dans notre arborescence, nous supprimons le prefix assets de la recherche - player.audioCache = AudioCache(prefix: ''); - - try { - //On tente de récupérer le fichier dans nos fichiers. Si une erreur est levée, c'est que le fichier n'existe pas (url incorrecte) - //Nous ne pouvons pas chercher directement si le fichier existe : les méthodes de Dart le permettant ne fonctionnent pas bien sous Android - await rootBundle.load(urlAudio); - } catch(_) { - rethrow; - } - - //On attend que le lecteur initialise notre fichier comme sa source. - await player.setSource(AssetSource(urlAudio)); - - //On retourne le lecteur audio - return player; - - } - - String? imageLoader(MediaCours data) { - if (data.type == "image") { - return data.url; // Retourne le chemin du fichier image - } - return null; // Retourne null si ce n'est pas une image - } -} - - diff --git a/lib/ui/Contenu/WidgetContenu/ContenuAudioWidget.dart b/lib/ui/Contenu/WidgetContenu/ContenuAudioWidget.dart deleted file mode 100644 index 22dbf18..0000000 --- a/lib/ui/Contenu/WidgetContenu/ContenuAudioWidget.dart +++ /dev/null @@ -1,288 +0,0 @@ -// ignore_for_file: must_be_immutable - -import 'package:audioplayers/audioplayers.dart'; -import 'package:flutter/material.dart'; -import 'package:seriouse_game/ui/Contenu/ContenuCoursViewModel.dart'; - -import 'ContenuVideoWidget.dart'; // Cet import sert à récupérer les fonctions de conversion de minutes en secondes déja créer dans ce fichier pour le lecteur vidéo - - -// ContenuAudioWidget sert à créer le widget responsable du lecteur audio de nos cours. - -class ContenuAudioWidget extends StatelessWidget { - - late String urlAudio; // Url permettant de localiser le fichier audio à jouer - late ContenuCoursViewModel fileLoader; // objet chargé d'initialiser le lecteur audio en utililisant les urlAudio - late AudioPlayer player; //lecteur audio - late bool error = false; // permet de gerer les erreurs remontés par le fileloader et gérer leur affichage - - //Constructeur permettant d'initialiser cette classe. Pour créer une instance de cette classe, on apelle ce constructeur de cette façon : - //ContenuAudioWidget(data: data) - ContenuAudioWidget({super.key, required this.urlAudio}){ - fileLoader = ContenuCoursViewModel(); - player = AudioPlayer(); - } - - //Fonction chargée d'initialisée le lecteur audio - Future initAudioPlayer() async { - try { - //initialisation du lecteur audio par le fileloader - player = await fileLoader.AudioLoader(urlAudio); - //Permet de faire jouer le lecteur en boucle. Attention désactiver cette option entraîne une suppression du lecteur à la fin de la lecture ! - await player.setReleaseMode(ReleaseMode.loop); - } catch(_){ - //Si une erreur est attrapée, c'est que le fichier audio n'as pas pu être trouvé : on indique qu'une erreur est survenue en passant error à true - error = true; - } - // On retourne le lecteur pour forçer le widget à attendre la fin de l'initialisation pour être build - return player; - } - - //Construction du widget Audio - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return FutureBuilder( - // On utilise FutureBuilder pour attendre la fin de l'initialisation du lecteur audio - future: initAudioPlayer(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - //Si aucune erreur n'a été retournée à la fin de l'initialisation du lecteur audio, on construit le widget audio. - //Voir plus bas pour le détail de cette classe - if (!error){ - return AudioPlayerScreen(player: player); - }else{ - return Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), - //Gestion de l'espace entre l'exterieur du widget et les widgets adjacents - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - //Décoration de la bordure - decoration: BoxDecoration( - //Gestion de l'angle de la bordure - borderRadius:BorderRadius.circular(15), - //Couleur interne et externe de la boite - color: Colors.white, - boxShadow: const [ - BoxShadow(color: Colors.black, spreadRadius: 0.5), - ], - ), - - child: - const Text("Audio file not found") - ); - } - //Si jamais le lecteur n'est toujours pas chargée on affiche une progress bar - }else{ - return const Center( - child: CircularProgressIndicator()); - } - } - ); - } - ); - } - - - -} -//Widget gérant le fonctionnement du lecteur audio -class AudioPlayerScreen extends StatefulWidget{ - final AudioPlayer player; - - const AudioPlayerScreen({super.key, required this.player}); - - @override - State createState() => _AudioPlayerScreenState(player); - -} - -//State de AudioPlayerScreen -class _AudioPlayerScreenState extends State { - - late AudioPlayer player; //Lecteur audio - bool isPlaying = false; // booleun indiquant si le lecteur audio est actuellement en train de jouer un son - Duration duration = Duration.zero; // Variable affichant la durée de l'audio dans le widget - Duration position = Duration.zero; // Variable affichant la position actuelle dans l'audio dans le widget - - _AudioPlayerScreenState(this.player); - - //Fonction permettant de récupérer la durée de l'audio auprès du lecteur - Future getDurationFromPlayer() async { - duration = (await player.getDuration())!; - } - - //Fonction permettant d'initialiser notre state - @override - void initState(){ - super.initState(); - - //Initialisation de la variable duration auprès du lecteur - getDurationFromPlayer(); - - //Initialisation d'un listener pour changer la valeur de isPlaying lorsque la vidéo est mise en pause ou relancée - player.onPlayerStateChanged.listen((state){ - setState(() { - isPlaying = state == PlayerState.playing; - }); - - }); - - //Initialisation d'un listener pour écouter les changement de la durée d'audio. - //A l'heure actuelle ce widget correspond à un seul et unique fichier Audio, la valeur de duration n'est donc pas censé changée ! - player.onDurationChanged.listen((state){ - - setState(() { - duration = state; - }); - - }); - - //Initialisation d'un listener pour mettre à jour position à chaque changement de la position dans le fichier audio - player.onPositionChanged.listen((state){ - - setState(() { - position = state; - }); - - }); - } - - //Build du widget - @override - Widget build(BuildContext context) { - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - - - if (player.source == null) { - //Permet d'afficher un message d'erreur si la source de l'audio (contenu du fichier audio initialisé) n'existe pas - return - Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), - //Gestion de l'espace entre l'exterieur du widget et les widgets adjacents - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - //Décoration de la bordure - decoration: BoxDecoration( - //Gestion de l'angle de la bordure - borderRadius:BorderRadius.circular(15), - //Couleur interne et externe de la boite - color: Colors.white, - boxShadow: const [ - BoxShadow(color: Colors.black, spreadRadius: 0.5), - ], - ), - - child: - const Text("An unexpected error has happened : audio player is null") - ); - - }else { - //Container pour afficher le widget sous la forme d'une boite arrondie - return Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), - //Gestion de l'espace entre l'exterieur du widget et les widgets adjacents - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - //Décoration de la bordure - decoration: BoxDecoration( - //Gestion de l'angle de la bordure - borderRadius:BorderRadius.circular(15), - //Couleur interne et externe de la boite - color: Colors.white, - boxShadow: const [ - BoxShadow(color: Colors.black, spreadRadius: 0.5), - ], - ), - - child: Row( - children : [ - - //Bouton permettant de retourner au début de la vidéo - IconButton( - onPressed: () { - setState(() { - player.seek(Duration.zero);//remet la position du lecteur à 0 - }); - }, - //Apparence du bouton - icon: const Icon( - Icons.restart_alt_rounded, - color: Color.fromARGB(255,232,165,99), - size: 20, - ), - ), - - //Espace entre les éléments du widget - const Spacer(), - - //Affichage de la position de la vidéo et de sa durée - Text('${convertToMinutesSeconds(position)} / ${convertToMinutesSeconds(duration)}'), - - //Espace entre les éléments du widget - const Spacer(), - - //Bouton permettant de retourner 10 secondes en arrière. - IconButton( - //Affichage du bouton - icon: const Icon( - Icons.replay_10, - color: Color.fromARGB(255,232,165,99),), - - onPressed: () { - //Position remise à la position actuelle du lecteur - 10 - player.seek(position-const Duration(seconds: 10)); - - }), - - //Bouton pause - - //Apparence du cercle derrière l'icone du bouton - CircleAvatar( - radius: 20, - backgroundColor:const Color.fromARGB(255,232,165,99), - //Bouton - child: IconButton( - //Choix de l'icone à afficher en fonction de si le lecteur joue l'audio ou non - icon: Icon(isPlaying - ? Icons.pause - : Icons.play_arrow, - color: Colors.white,), - - onPressed: () { - setState(() { - //Met le lecteur en pause ou relance l'audio en fonction si le lecteur était déjé en pause ou non. - isPlaying - ? {player.pause()} - : {player.resume()}; - }); - }, - ), - ), - - //Bouton permettant d'avancer de 10 secondes - IconButton( - //Affichage du bouton - icon: const Icon( - Icons.forward_10, - color: Color.fromARGB(255,232,165,99),), - - onPressed: () { - //Position avancée à la position actuelle du lecteur + 10 - player.seek(position+const Duration(seconds: 10)); - - }), - - ] - ), - ); - } - - - }, - ); - } -} \ No newline at end of file diff --git a/lib/ui/Contenu/WidgetContenu/ContenuImageWidget.dart b/lib/ui/Contenu/WidgetContenu/ContenuImageWidget.dart deleted file mode 100644 index 0e0c69b..0000000 --- a/lib/ui/Contenu/WidgetContenu/ContenuImageWidget.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:seriouse_game/models/mediaCours.dart'; -import 'package:seriouse_game/ui/Contenu/ContenuCoursViewModel.dart'; - -class ContenuImageWidget extends StatelessWidget { - final MediaCours media; - final double width; - final double height; - final ContenuCoursViewModel viewModel = ContenuCoursViewModel(); - - ContenuImageWidget({ - super.key, - required this.media, - this.width = 200, // Valeur par défaut - this.height = 200, // Valeur par défaut - }); - - @override - Widget build(BuildContext context) { - String? imagePath = viewModel.imageLoader(media); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), // Padding léger - child: imagePath != null - ? Image.asset( - imagePath, - width: width, // Largeur de l'image - height: height, // Hauteur de l'image - fit: BoxFit.cover, - semanticLabel: media.caption, // Accessibilité - errorBuilder: (context, error, stackTrace) { - return const Icon(Icons.broken_image, size: 50, color: Colors.grey); - }, - ) - : const Icon(Icons.image_not_supported, size: 50, color: Colors.grey), - ); - } -} diff --git a/lib/ui/Contenu/WidgetContenu/ContenuTextWidget.dart b/lib/ui/Contenu/WidgetContenu/ContenuTextWidget.dart deleted file mode 100644 index 24ec4d5..0000000 --- a/lib/ui/Contenu/WidgetContenu/ContenuTextWidget.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class ContenuTextWidget extends StatelessWidget { - final String filePath; // Chemin vers le fichier asset - - const ContenuTextWidget({Key? key, required this.filePath}) : super(key: key); - - // Fonction pour charger le texte depuis un asset - Future _loadTextFromFile(String filePath) async { - return await rootBundle.loadString(filePath); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _loadTextFromFile(filePath), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return const Center( - child: Text( - 'Erreur : Impossible de lire le fichier.', - style: TextStyle(color: Color.fromRGBO(252, 179, 48, 1)), - ), - ); - } else { - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Text( - snapshot.data ?? '', - style: const TextStyle(fontSize: 16.0, height: 1.5), - ), - ); - } - }, - ); - } -} diff --git a/lib/ui/Contenu/WidgetContenu/ContenuVideoWidget.dart b/lib/ui/Contenu/WidgetContenu/ContenuVideoWidget.dart deleted file mode 100644 index 7b369b2..0000000 --- a/lib/ui/Contenu/WidgetContenu/ContenuVideoWidget.dart +++ /dev/null @@ -1,569 +0,0 @@ -// ignore_for_file: must_be_immutable -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:seriouse_game/models/mediaCours.dart'; -import 'package:seriouse_game/ui/Contenu/ContenuCoursViewModel.dart'; -import 'package:video_player/video_player.dart'; - -//Classe chargée de build un widget vidéo à partir des données contenues dans un MediaCours -class ContenuVideoWidget extends StatelessWidget { - - late MediaCours data;//Les données correspondant à la vidéo à charger - late ContenuCoursViewModel fileLoader;// objet chargé d'initialiser le lecteur vidéo en utililisant les données de data - late VideoPlayerController controller;//Lecteur vidéo - bool error = false;// permet de gerer les erreurs remontés par le fileloader et gérer leur affichage - - //Constructeur permettant d'initialiser cette classe. Pour créer une instance de cette classe, on apelle ce constructeur de cette façon : - //ContenuVideoWidget(data: data) - ContenuVideoWidget({super.key, required this.data}){ - fileLoader = ContenuCoursViewModel(); - } - - //Méthode chargée d'initialisée le lecteur vidéo - Future initController() async { - - //Création d'un lecteur vidéo vide pour éviter une erreur de variable non initialisée. - //Cette valeur ne sera cependant jamais utilisé. - //Il n'existe aucune méthode permettant de créer un VideoPlayerController sans lui passer une ressource - controller = VideoPlayerController.asset(""); - - //Le code suivant va tenter de changer la valeur de notre lecteur vidéo avec les données contenues dans data. - //Si aucun fichier ne correspond aux données transmises, on change la valeur de "error" à true - try { - //On apelle fileloader pour extraire la vidéo de notre système de fichier - controller = await fileLoader.VideoLoader(data); - } catch(_) { - error = true; - } - //On retourne error à la fin pour forçer le widget à attendre la fin de l'initialisation. - return error; - - } - - @override - Widget build(BuildContext context) { - - //Ici FutureBuilder nous permet d'attendre la fin de notre initialisation pour build le widget. - return FutureBuilder( - future: initController(),//Indique que l'ont doit attendre le retour de la valeur de initController pour build le widget. - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - //Si aucune erreur n'as été reporté durant l'initialisation du lecteur, alors on build une instance de VideoPlayerScreen, widget permettant de jouer la vidéo - if(!error){ - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return VideoPlayerScreen(controller: controller); - }); - //En cas d'erreur, on indique qu'aucun fichier vidéo n'as été trouvé. - }else { - return const Scaffold( - body: Center( - child: - SizedBox( - child: Text("Video file not found"), - ) - ) - - ); - } - //Affiche un ecran de chargement tant que le lecteur vidéo n'est pas prêt. - }else{ - return const Center( - child: CircularProgressIndicator(), - ); - } - - } - - ); - } -} - -//Widget affichant la vidéo à l'écran -class VideoPlayerScreen extends StatefulWidget { - - //Lecteur vidéo - final VideoPlayerController controller; - - const VideoPlayerScreen({Key? key, required this.controller}) : super(key: key); - - @override - State createState() => _VideoPlayerScreenState(controller); -} - -//State du widget d'affichage de la vidéo -class _VideoPlayerScreenState extends State { - - late VideoPlayerController _controller;//Lecteur vidéo - late Future _initializeVideoPlayerFuture;//Future lié à l'initialisation de la lecture de la vidéo. - //Sert par la suite à synchronizer le build du widget à la fin de l'initialisation - - Duration videoLength = const Duration();//Durée de la vidéo - Duration videoPosition = const Duration();//Position actuelle dans la vidéo - - bool errorLoadingVideo = false;//Indique si une erreur à été s'est produite lors du lancement de la vidéo. - double volume = 0.5;//Volume sonore de la vidéo - - bool isFullscreen = false;//Indique si la vidéo est en plein écran ou non. - - //Indique si les boutons du mode fullscreen doivent être visibles ou non - //Lorsque la vidéo est jouée en plein écran aucun bouton n'est visible tant que l'utilisateur ne touche pas à l'écran. - bool fullscreenWidgetButtons = false; - bool HasVideoNotStarted = true;//Indique si la vidéo à déjà commencé à jouer au moins une fois. - - _VideoPlayerScreenState(VideoPlayerController controller){ - _controller = controller; - } - - //Méthode gérant le timer permettant de maintenir l'affichage des boutons en plein écran 3 secondes après que l'utilisateur ait touché l'écran. - void startTimerWidgetFullscreen() { - fullscreenWidgetButtons = true; - Timer(const Duration(seconds: 3), handleTimeoutWidgetFullscreen); - } - - //Passe fullscreenWidgetButtons à 'false' pour permettre de désactiver l'affichage des boutons une fois le timer de startTimerWidgetFullscreen terminé - void handleTimeoutWidgetFullscreen() { - fullscreenWidgetButtons = false; - } - - @override - //Initialisation de notre State. - //Va permettre d'initialiser toutes les variables liées au lecteur vidéo - void initState() { - - super.initState(); - - //On tente de charger la vidéo contenue dans le fichier passer au lecteur. - try { - _initializeVideoPlayerFuture = _controller.initialize().then((_) => setState(() { - - try { - //On initialise la valeur de la durée de la vidéo - videoLength = _controller.value.duration; - }catch (_) { - errorLoadingVideo = true; - } - })); - - //Si la vidéo a pu être chargée, on active la lecture en boucle - _controller.setLooping(true); - - //Ajout de deux listener : - //L'un permet de synchroniser notre variable videoPosition à la position dans la vidéo. - //Le deuxième permet de détecter si une erreur survient lors de la lecture de la vidéo. - _controller.addListener(() => setState(() { - videoPosition = _controller.value.position; - errorLoadingVideo = _controller.value.hasError; - })); - - }catch (_) { - errorLoadingVideo = true; - } - } - - //Méthode permettant de détruire le lecteur vidéo à la destruction du widget. - @override - void dispose() { - - _controller.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - - //En cas d'erreur lors du chargement de la vidéo, on affiche un message d'erreur à l'écran. - if(errorLoadingVideo){ - return ( - const Scaffold( - body: Center( - child: - SizedBox( - child: Text("Video can't be loaded"), - ) - ) - - ) - ); - }else { - - //On detecte si l'ecran est au format portrait ou paysage - //L'apparence et les interactions avec le widget sont différentes en fonction de l'orientation(mode normal/plein écran) - if (MediaQuery.of(context).orientation == Orientation.portrait) { - exitFullScreen();//Méthode permettant de quitter le plein ecran et changer l'orientation en portrait - isFullscreen = false;//On indique que l'on est plus en plein écran. - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - - children: [ - - //On utilise un gesture detector pour detecter si l'utilisateur appuie sur l'écran, et mettre la vidéo en pause ou la relancer. - GestureDetector( - behavior: HitTestBehavior.opaque, - //Si la vidéo est en train d'être jouer, on met en pause, sinon on relance la vidéo. - onTap: () => - _controller.value.isPlaying ? _controller.pause() : _controller.play(), - child: - //Widget d'affichage de la vidéo - videoScreen() - ), - - //Barre de progression indiquant la position dans la vidéo (cf barre rouge de youtube) - progressBarVideo(), - Row( - children : [ - //Bouton Lecture/Pause : - playButton(Colors.black), - - //Affichage icône pour le volume sonore. - //animatedVolumeIcon est une fonction renvoyant une icône differente en fonction du niveau sonore. - Icon(animatedVolumeIcon(volume)), - - //Barre gérant le volume sonore - volumeSlider(), - - //Créer un espace entre les boutons de l'interface - const Spacer(), - - //Affiche la position de la vidéo sur le temps restant en minute & secondes. - Text( - '${convertToMinutesSeconds(videoPosition)} / ${convertToMinutesSeconds(videoLength)}'), - const SizedBox(width: 10), - - //Créer un espace entre les boutons de l'interface - const Spacer(), - - //Bouton permettant de revenir au début de la lecture de la vidéo - returnToStart(Colors.black), - - - //Bouton permettant de passer en mode plein écran - fullscreenButton(Colors.black) - ], - ), - ], - - - ); - } - - ); - }else{ - //On apelle la méthode pour entrer en fullscreen si l'orientation paysage est détectée - enterFullScreen(); - //On indique que l'on est en plein écran - isFullscreen = true; - return ( - Scaffold( - body: Center( - child: - LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //Stack va nous permettre d'avoir une interface flottante par dessus la vidéo (pour nos boutons) - //Cette interface ne sera affichée que temporairement après un appui de l'utilisateur - return Stack( - children: [ - SizedBox( - //Force la vidéo à prendre la taille maximul allouée par le widget parent si cette taille n'est pas inférieure à la taille miniumum du controleur - height: constraints.maxHeight, - width: constraints.maxWidth, - child : - //Permet de detecter que l'utilisateur appuie sur l'écran - GestureDetector( - behavior: HitTestBehavior.opaque, - //Si l'interface est déja affichée , on la cache, sinon on lance le timer et leur affichage - onTap: () => { - fullscreenWidgetButtons ? fullscreenWidgetButtons = false : startTimerWidgetFullscreen(), - }, - child: - //On affiche la vidéo - videoScreen() - ), - - - ), - //Widget visible seulement sous certaines conditions. - //Il représente l'interface sur la vidéo (similaire à ytb : boutons, bar de progression, ect...) - Visibility( - maintainSize: true, - maintainAnimation: true, - maintainState: true, - //Le widget n'est visible que si les boutons sont activés (fullscreenWidgetButton) par un appui utilisateur - //On a cependant ajouté une condition supplémentaire : si la vidéo n'est pas encore lancée l'interface est visible - //Cette condition est nécessaire car tant qu'elle ne sera pas lancé, le GestureDetector contenant le lecteur ne fonctionne pas - //Cela est du au contrôleur de la vidéo controller. - //Il s'agit de la seule utilité de la variable HasVideoNotStarted - visible: fullscreenWidgetButtons || HasVideoNotStarted, - child: - - Column( - children: [ - - const Spacer(), - - //Bouton Play/Pause au centre de l'écran. - Align( - alignment: Alignment.center, - child: SizedBox( - width: 70, - height: 70, - child: IconButton( - icon: Icon(_controller.value.isPlaying - ? Icons.pause - : Icons.play_arrow), - color: Colors.white, - iconSize: 70, - - onPressed: () { - setState(() { - _controller.value.isPlaying - ? _controller.pause() - : {_controller.play(), HasVideoNotStarted = false}; - } - - ); - startTimerWidgetFullscreen(); - }, - ), - - ), - ), - - const Spacer(), - - //Interface en bas de l'écran. Elle est similaire à celle en mode portrait - Align( - alignment: Alignment.bottomCenter, - child : SizedBox( - //Les ratios utilisés ici sont fait pour tenir dans l'écran avec le header et le footer de l'app - width: constraints.maxWidth, - height : 110, - child: - Column( - children: [ - //Bar de progression style ytb - progressBarVideo(), - Row( - - children : [ - //Bouton play/Pause - playButton(Colors.white), - - //Volume - Icon(animatedVolumeIcon(volume), - color: Colors.white), - - volumeSlider(), - - const Spacer(), - - //Affichage position de la vidéo/durée totale de la vidéo - Text( - '${convertToMinutesSeconds(videoPosition)} / ${convertToMinutesSeconds(videoLength)}', - style : const TextStyle( - fontSize: 15.0, - color: Colors.white, - ), - ), - - const Spacer(), - - //Bouton restart - returnToStart(Colors.white), - - //Bouton de gestion passage entre portrait et plein écran - fullscreenButton(Colors.white) - ], - ), - ], - ), - ), - ), - ], - ), - ), - ] - ); - } - ) - ) - - ) - ); - } - }} - - //Bouto permettant de changer entre plein ecran(paysage) et portrait - IconButton fullscreenButton(Color color){ - return IconButton( - onPressed: () { - //On teste si on est en plein écran ou non - //Si oui, on passe isFullscreen à false, on change l'orientation de l'appli en portrait, et on rétablit ensuite toutes les rotations (les rotations sont bloquées en fullscreen) - //Si non, on passe isFullscreen à true et on passe en paysage. On active également le timer startTimerWidgetFullscreen pour que l'interface soit visible 3 secondes après le changement d'orientation. - isFullscreen - ? {isFullscreen = false, exitFullScreen(), SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),enableRotation()} - : {isFullscreen = true, enterFullScreen(), SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft,DeviceOrientation.landscapeRight]),startTimerWidgetFullscreen()}; - }, - icon: Icon( - Icons.fullscreen, - color: color, - size: 20, - ), - ); - } - - //Bouton permettant de revenir au début de la lecture de la vidéo - IconButton returnToStart(Color color){ - return IconButton( - //On change la position du lecteur vidéo à 0. - onPressed: () { - setState(() { - _controller.seekTo(Duration.zero); - }); - }, - icon: Icon( - Icons.restart_alt_rounded, - color: color, - size: 20, - ), - ); - } - //Barre gérant le volume sonore - SizedBox volumeSlider(){ - - return SizedBox( - width: 100, - child: Slider( - value: volume, - min: 0, - max: 1, - //Code changeant le volume quand la barre est manipulée par l'utilisateur : - onChanged: (volume) => setState(() { - volume = volume; - _controller.setVolume(volume); - - } - ), - ), - ); - } - //Bouton Lecture/Pause : - IconButton playButton(Color color){ - return - IconButton( - //Icone change en fonction de si la vidéo est en pause ou non : - icon: Icon(_controller.value.isPlaying - ? Icons.pause - : Icons.play_arrow, - size: 20, - color: color,), - - //Si le lecteur joue la vidéo, on la met en pause, sinon on la joue. - //On met également HasVideoNotStarted à false si on joue la vidéo - onPressed: () { - setState(() { - _controller.value.isPlaying - ? _controller.pause() - : {_controller.play(), HasVideoNotStarted = false}; - }); - if(isFullscreen){ - startTimerWidgetFullscreen(); - } - }, - ); - } - - //Barre de progression indiquant la position dans la vidéo (cf barre rouge de youtube) - Slider progressBarVideo(){ - return Slider( - //Variable indiquant la valeur actuelle de la progression (position dans la vidéo) - value: videoPosition.inSeconds.toDouble(), - min: 0, - //La valeur maximale de cette barre est la durée de la vidéo - max: videoLength.inSeconds.toDouble(), - - thumbColor: const Color.fromARGB(255, 246, 1, 1), - activeColor: const Color.fromARGB(255, 246, 1, 1), - - //Code appliqué lorsque l'utilisateur appuie et change la valeur de la barre : - onChanged: (videoPosition) => setState(() { - //Code permettant de déplacer la position de la vidéo à celle de la barre : - videoPosition = (Duration.zero + Duration(seconds: videoPosition.toInt())) as double; - _controller.seekTo(videoPosition as Duration); - - } - ), - ); - } - //FutureBuilder permettant d'attendre la fin du chargement de la vidéo pour l'afficher - FutureBuilder videoScreen(){ - return FutureBuilder( - future: _initializeVideoPlayerFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - //Affichage de la vidéo : - return AspectRatio( - aspectRatio: _controller.value.aspectRatio, - - child: VideoPlayer(_controller), - ); - } else { - //Affichage d'un écran de chargement si la vidéo n'est pas chargée. - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - } - - -} - -//Permet de convertir un objet Duration en une string représentant sa durée en minute,secondes. -String convertToMinutesSeconds(Duration duration) { - final parsedMinutes = duration.inMinutes < 10 - ? '0${duration.inMinutes}' - : duration.inMinutes.toString(); - - final seconds = duration.inSeconds % 60; - - final parsedSeconds = - seconds < 10 ? '0${seconds % 60}' : (seconds % 60).toString(); - return '$parsedMinutes:$parsedSeconds'; -} - -//Fonction permettant de renvoyer une icone de volume différente en fonction d'une valeur entre 0 et 1. -IconData animatedVolumeIcon(double volume) { - if (volume == 0) { - return Icons.volume_mute; - } else if (volume < 0.5){ - return Icons.volume_down; - }else{ - return Icons.volume_up; - } -} - -//Change l'orientation de l'application pour mettre en plein ecran -void enterFullScreen() { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); -} - -//Change l'orientation de l'application pour mettre en portrait -void exitFullScreen() { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); -} - -//Fixe l'orientation préférée de l'appareil à toutes les rotations possibles pour les rétablir. -void enableRotation() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); -} diff --git a/lib/ui/Contenu/WidgetContenu/contenu_audio_widget.dart b/lib/ui/Contenu/WidgetContenu/contenu_audio_widget.dart new file mode 100644 index 0000000..f0c81f3 --- /dev/null +++ b/lib/ui/Contenu/WidgetContenu/contenu_audio_widget.dart @@ -0,0 +1,217 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/material.dart'; +import 'package:factoscope/ui/Contenu/contenu_cours_view_model.dart'; + +class ContenuAudioWidget extends StatefulWidget { + final String urlAudio; + + const ContenuAudioWidget({super.key, required this.urlAudio}); + + @override + State createState() => _ContenuAudioWidgetState(); +} + +class _ContenuAudioWidgetState extends State { + late ContenuCoursViewModel fileLoader; + late AudioPlayer player; + bool error = false; + bool isLoading = true; + + @override + void initState() { + super.initState(); + fileLoader = ContenuCoursViewModel(); + player = AudioPlayer(); + _initAudio(); + } + + Future _initAudio() async { + try { + player = await fileLoader.audioLoader(widget.urlAudio); + await player.setReleaseMode(ReleaseMode.loop); + } catch (_) { + if (mounted) setState(() => error = true); + } finally { + if (mounted) setState(() => isLoading = false); + } + } + + @override + void dispose() { + player.dispose(); + super.dispose(); + } + + BoxDecoration get _cardDecoration => BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.white, + border: Border.all( + color: const Color.fromARGB(255, 3, 47, 122), + width: 3, + ), + ); + + @override + Widget build(BuildContext context) { + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (error) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), + margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + decoration: _cardDecoration, + child: const Text("Audio file not found"), + ); + } + + return AudioPlayerScreen(player: player, cardDecoration: _cardDecoration); + } +} + +class AudioPlayerScreen extends StatefulWidget { + final AudioPlayer player; + final BoxDecoration cardDecoration; + + const AudioPlayerScreen({ + super.key, + required this.player, + required this.cardDecoration, + }); + + @override + State createState() => _AudioPlayerScreenState(); +} + +class _AudioPlayerScreenState extends State { + bool isPlaying = false; + Duration duration = Duration.zero; + Duration position = Duration.zero; + + AudioPlayer get player => widget.player; + + @override + void initState() { + super.initState(); + + player.onPlayerStateChanged.listen((state) { + if (mounted) setState(() => isPlaying = state == PlayerState.playing); + }); + + player.onDurationChanged.listen((d) { + if (mounted) setState(() => duration = d); + }); + + player.onPositionChanged.listen((p) { + if (mounted) setState(() => position = p); + }); + + // Récupère la durée initiale si déjà dispo + player.getDuration().then((d) { + if (d != null && mounted) setState(() => duration = d); + }); + } + + void _seekBy(Duration offset) { + final newPos = position + offset; + final clamped = newPos < Duration.zero + ? Duration.zero + : (newPos > duration ? duration : newPos); + player.seek(clamped); + } + + String _format(Duration d) { + final m = d.inMinutes.remainder(60).toString().padLeft(2, '0'); + final s = d.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$m:$s'; + } + + @override + Widget build(BuildContext context) { + final progress = duration.inMilliseconds > 0 + ? position.inMilliseconds / duration.inMilliseconds + : 0.0; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + decoration: widget.cardDecoration, + child: Column( + children: [ + // Barre de progression + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: const Color.fromRGBO(252, 179, 48, 1), + inactiveTrackColor: Colors.grey.shade300, + thumbColor: const Color.fromRGBO(252, 179, 48, 1), + overlayColor: + const Color.fromRGBO(252, 179, 48, 1).withValues(alpha: 0.2), + trackHeight: 3.0, + thumbShape: + const RoundSliderThumbShape(enabledThumbRadius: 6.0), + ), + child: Slider( + value: progress.clamp(0.0, 1.0), + onChanged: (value) { + final newPos = + Duration(milliseconds: (value * duration.inMilliseconds).round()); + player.seek(newPos); + }, + ), + ), + // Contrôles + Row( + children: [ + // Restart + IconButton( + onPressed: () => player.seek(Duration.zero), + icon: const Icon( + Icons.restart_alt_rounded, + color: Color.fromRGBO(252, 179, 48, 1), + size: 20, + ), + ), + const Spacer(), + // Timer + Text( + '${_format(position)} / ${_format(duration)}', + style: const TextStyle(fontSize: 12), + ), + const Spacer(), + // -10s + IconButton( + icon: const Icon( + Icons.replay_10, + color: Color.fromRGBO(252, 179, 48, 1), + ), + onPressed: () => _seekBy(const Duration(seconds: -10)), + ), + // Play/Pause + CircleAvatar( + radius: 20, + backgroundColor: const Color.fromRGBO(252, 179, 48, 1), + child: IconButton( + icon: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + ), + onPressed: () => + isPlaying ? player.pause() : player.resume(), + ), + ), + // +10s + IconButton( + icon: const Icon( + Icons.forward_10, + color: Color.fromRGBO(252, 179, 48, 1), + ), + onPressed: () => _seekBy(const Duration(seconds: 10)), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/Contenu/WidgetContenu/contenu_image_widget.dart b/lib/ui/Contenu/WidgetContenu/contenu_image_widget.dart new file mode 100644 index 0000000..5563bb6 --- /dev/null +++ b/lib/ui/Contenu/WidgetContenu/contenu_image_widget.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:factoscope/models/page.dart'; + +class ContenuImageWidget extends StatelessWidget { + final MediaItem media; + final double width; + final double height; + + const ContenuImageWidget({ + super.key, + required this.media, + this.width = 200, + this.height = 200, + }); + + @override + Widget build(BuildContext context) { + final fichier = File(media.url); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Center( + child: Column( + children: [ + Image.file( + fichier, + width: width, + height: height, + fit: BoxFit.cover, + semanticLabel: media.caption, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.broken_image, + size: 50, color: Colors.grey); + }, + ), + if (media.caption != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + media.caption!, + style: const TextStyle( + fontSize: 12, fontStyle: FontStyle.italic), + textAlign: TextAlign.center, + ), + ), + ], + ), + )); + } +} \ No newline at end of file diff --git a/lib/ui/Contenu/WidgetContenu/contenu_text_widget.dart b/lib/ui/Contenu/WidgetContenu/contenu_text_widget.dart new file mode 100644 index 0000000..8c8983e --- /dev/null +++ b/lib/ui/Contenu/WidgetContenu/contenu_text_widget.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; + +class ContenuTextWidget extends StatelessWidget { + final String filePath; + + const ContenuTextWidget({super.key, required this.filePath}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: rootBundle.loadString(filePath), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return const Text("Erreur de chargement du texte"); + } + return Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + snapshot.data ?? '', + style: const TextStyle(fontSize: 16), + ), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); + } +} diff --git a/lib/ui/Contenu/WidgetContenu/contenu_video_widget.dart b/lib/ui/Contenu/WidgetContenu/contenu_video_widget.dart new file mode 100644 index 0000000..cae2d4d --- /dev/null +++ b/lib/ui/Contenu/WidgetContenu/contenu_video_widget.dart @@ -0,0 +1,343 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:factoscope/models/page.dart'; +import 'package:video_player/video_player.dart'; + +class ContenuVideoWidget extends StatefulWidget { + final MediaItem data; + + const ContenuVideoWidget({super.key, required this.data}); + + @override + State createState() => _ContenuVideoWidgetState(); +} + +class _ContenuVideoWidgetState extends State { + late VideoPlayerController _controller; + bool _error = false; + bool _loading = true; + + @override + void initState() { + super.initState(); + _initController(); + } + + Future _initController() async { + try { + await rootBundle.load(widget.data.url); + _controller = VideoPlayerController.asset(widget.data.url); + await _controller.initialize(); + _controller.setLooping(true); + } catch (_) { + if (mounted) setState(() => _error = true); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + void dispose() { + if (!_error && !_loading) _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_loading) return const Center(child: CircularProgressIndicator()); + if (_error) { + return const Center(child: Text("Vidéo introuvable")); + } + return VideoPlayerScreen(controller: _controller); + } +} + + +class VideoPlayerScreen extends StatefulWidget { + final VideoPlayerController controller; + + const VideoPlayerScreen({super.key, required this.controller}); + + @override + State createState() => _VideoPlayerScreenState(); +} + +class _VideoPlayerScreenState extends State { + VideoPlayerController get _controller => widget.controller; + + Duration _videoLength = Duration.zero; + Duration _videoPosition = Duration.zero; + double _volume = 0.5; + bool _isFullscreen = false; + bool _showControls = true; // toujours visible en portrait mais bon... temporaire en fullscreen + bool _hasStarted = false; + Timer? _hideControlsTimer; + + @override + void initState() { + super.initState(); + + _videoLength = _controller.value.duration; + _controller.setVolume(_volume); + + _controller.addListener(_onControllerUpdate); + } + + void _onControllerUpdate() { + if (!mounted) return; + setState(() { + _videoPosition = _controller.value.position; + _videoLength = _controller.value.duration; + }); + } + + @override + void dispose() { + _hideControlsTimer?.cancel(); + _controller.removeListener(_onControllerUpdate); + super.dispose(); + } + + void _enterFullscreen() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + + void _exitFullscreen() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + + void _toggleFullscreen() { + setState(() => _isFullscreen = !_isFullscreen); + _isFullscreen ? _enterFullscreen() : _exitFullscreen(); + if (_isFullscreen) _startHideControlsTimer(); + } + + void _startHideControlsTimer() { + _hideControlsTimer?.cancel(); + _hideControlsTimer = Timer(const Duration(seconds: 3), () { + if (mounted) setState(() => _showControls = false); + }); + } + + void _onTapFullscreen() { + setState(() => _showControls = !_showControls); + if (_showControls) _startHideControlsTimer(); + } + + void _togglePlay() { + setState(() { + if (_controller.value.isPlaying) { + _controller.pause(); + } else { + _controller.play(); + _hasStarted = true; + } + }); + if (_isFullscreen) _startHideControlsTimer(); + } + + Widget _videoDisplay() { + return AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ); + } + + Slider _progressBar() { + final maxVal = _videoLength.inMilliseconds.toDouble(); + final currentVal = _videoPosition.inMilliseconds + .toDouble() + .clamp(0.0, maxVal > 0 ? maxVal : 1.0); + + return Slider( + value: currentVal, + min: 0, + max: maxVal > 0 ? maxVal : 1.0, + thumbColor: const Color.fromARGB(255, 246, 1, 1), + activeColor: const Color.fromARGB(255, 246, 1, 1), + onChanged: (value) { + _controller.seekTo(Duration(milliseconds: value.toInt())); + }, + ); + } + + Widget _volumeControl(Color iconColor) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(_volumeIcon(_volume), color: iconColor), + SizedBox( + width: 90, + child: Slider( + value: _volume, + min: 0, + max: 1, + onChanged: (val) { + setState(() => _volume = val); + _controller.setVolume(val); + }, + ), + ), + ], + ); + } + + IconData _volumeIcon(double vol) { + if (vol == 0) return Icons.volume_mute; + if (vol < 0.5) return Icons.volume_down; + return Icons.volume_up; + } + + IconButton _playButton(Color color) { + return IconButton( + icon: Icon( + _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, + size: 20, + color: color, + ), + onPressed: _togglePlay, + ); + } + + IconButton _restartButton(Color color) { + return IconButton( + onPressed: () => _controller.seekTo(Duration.zero), + icon: Icon(Icons.restart_alt_rounded, color: color, size: 20), + ); + } + + IconButton _fullscreenButton(Color color) { + return IconButton( + onPressed: _toggleFullscreen, + icon: Icon( + _isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, + color: color, + size: 20, + ), + ); + } + + Widget _timeDisplay(Color color) { + return Text( + '${_format(_videoPosition)} / ${_format(_videoLength)}', + style: TextStyle(color: color, fontSize: 13), + ); + } + + String _format(Duration d) { + final m = d.inMinutes.remainder(60).toString().padLeft(2, '0'); + final s = d.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$m:$s'; + } + + @override + Widget build(BuildContext context) { + final isPortrait = + MediaQuery.of(context).orientation == Orientation.portrait; + + if (isPortrait && _isFullscreen) { + setState(() => _isFullscreen = false); + _exitFullscreen(); + } + + return isPortrait ? _buildPortrait() : _buildFullscreen(); + } + + Widget _buildPortrait() { + return Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _togglePlay, + child: _videoDisplay(), + ), + _progressBar(), + Row( + children: [ + _playButton(Colors.black), + _volumeControl(Colors.black), + const Spacer(), + _timeDisplay(Colors.black), + const Spacer(), + _restartButton(Colors.black), + _fullscreenButton(Colors.black), + ], + ), + ], + ); + } + + Widget _buildFullscreen() { + return Scaffold( + backgroundColor: Colors.black, + body: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _onTapFullscreen, + child: Stack( + children: [ + Center(child: _videoDisplay()), + AnimatedOpacity( + opacity: (_showControls || !_hasStarted) ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: Container( + color: Colors.black26, + child: Column( + children: [ + const Spacer(), + // Bouton play central + Center( + child: IconButton( + icon: Icon( + _controller.value.isPlaying + ? Icons.pause + : Icons.play_arrow, + color: Colors.white, + size: 60, + ), + onPressed: _togglePlay, + ), + ), + const Spacer(), + // Barre du bas + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + children: [ + _progressBar(), + Row( + children: [ + _playButton(Colors.white), + _volumeControl(Colors.white), + const Spacer(), + _timeDisplay(Colors.white), + const Spacer(), + _restartButton(Colors.white), + _fullscreenButton(Colors.white), + ], + ), + ], + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/Contenu/contenu_cours_view.dart b/lib/ui/Contenu/contenu_cours_view.dart new file mode 100644 index 0000000..77ed646 --- /dev/null +++ b/lib/ui/Contenu/contenu_cours_view.dart @@ -0,0 +1,91 @@ +import 'package:factoscope/ui/Contenu/WidgetContenu/contenu_audio_widget.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/ui/Contenu/WidgetContenu/contenu_image_widget.dart'; +import 'package:factoscope/ui/Contenu/WidgetContenu/contenu_text_widget.dart'; +import 'package:factoscope/ui/Contenu/WidgetContenu/contenu_video_widget.dart'; + +class ContenuCoursView extends StatelessWidget { + final Cours cours; + final int selectedPageIndex; + + const ContenuCoursView({ + super.key, + required this.cours, + required this.selectedPageIndex, + }); + + @override + Widget build(BuildContext context) { + if (cours.pages == null || + selectedPageIndex < 0 || + selectedPageIndex >= cours.pages!.length) { + if (kDebugMode) { + print("Page introuvable"); + } + return const Center(child: Text("Page introuvable")); + } + + var page = cours.pages![selectedPageIndex]; + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (page.description != null) + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + page.description!, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + + if (page.contenu != null && page.contenu!.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Text( + page.contenu!, + style: const TextStyle(fontSize: 15, height: 1.6), + ), + ), + + // Médias + if (page.medias != null && page.medias!.isNotEmpty) + ...page.medias!.map((media) { + if (kDebugMode) { + print("Media url: ${media.url}, type: ${media.type}"); + } + if (media.type == "image") { + return Padding( + padding: const EdgeInsets.all(8.0), + child: ContenuImageWidget(media: media), + ); + } else if (media.type == "video") { + return Padding( + padding: const EdgeInsets.all(8.0), + child: ContenuVideoWidget(data: media), + ); + } else if (media.type == "audio") { + return Padding( + padding: const EdgeInsets.all(8.0), + child: ContenuAudioWidget(urlAudio: media.url,), + ); + } else if (media.type == "text") { + return Padding( + padding: const EdgeInsets.all(8.0), + child: ContenuTextWidget(filePath: media.url), + ); + } else { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Text("Le Media n'a pas le bon type !"), + ); + } + }), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/Contenu/contenu_cours_view_model.dart b/lib/ui/Contenu/contenu_cours_view_model.dart new file mode 100644 index 0000000..ac90008 --- /dev/null +++ b/lib/ui/Contenu/contenu_cours_view_model.dart @@ -0,0 +1,53 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/services.dart'; +import 'package:factoscope/models/page.dart'; +import 'package:video_player/video_player.dart'; + +class ContenuCoursViewModel { + ContenuCoursViewModel(); + + // Méthode permettant d'initialiser un lecteur vidéo avec l'url d'un fichier + Future videoLoader(MediaItem mediaModel) async { + late VideoPlayerController controller; + + // On teste si le type de média est bien celui voulu + if (mediaModel.type != "video") { + throw Exception("Wrong type of ressources"); + } + + // On vérifie si le fichier vidéo existe + try { + await rootBundle.load(mediaModel.url); + } on Exception { + rethrow; + } + + // On initialise et retourne le controller vidéo + controller = VideoPlayerController.asset(mediaModel.url); + return controller; + } + + // Méthode permettant d'initialiser un lecteur audio + Future audioLoader(String urlAudio) async { + // Création du lecteur audio + final player = AudioPlayer(); + // Par défaut AudioPlayer cherche les fichiers audios dans le dossier assets + player.audioCache = AudioCache(prefix: ''); + + try { + await rootBundle.load(urlAudio); + } catch (_) { + rethrow; + } + + await player.setSource(AssetSource(urlAudio)); + return player; + } + + String? imageLoader(MediaItem data) { + if (data.type == "image") { + return data.url; + } + return null; + } +} \ No newline at end of file diff --git a/lib/ui/Cours/CoursView.dart b/lib/ui/Cours/CoursView.dart deleted file mode 100644 index e672f49..0000000 --- a/lib/ui/Cours/CoursView.dart +++ /dev/null @@ -1,185 +0,0 @@ -// ignore_for_file: must_be_immutable - -import 'package:flutter/material.dart'; - -import 'package:seriouse_game/ui/Cours/CoursViewModel.dart'; -import 'package:seriouse_game/ui/Description/DescriptionView.dart'; -import 'package:seriouse_game/ui/Contenu/ContenuCoursView.dart'; -import 'package:seriouse_game/ui/QCM/JeuQCMView.dart'; - - -import 'package:seriouse_game/ui/CoursSelectionne.dart'; -import 'package:seriouse_game/models/cours.dart'; - -import 'package:go_router/go_router.dart'; - -class CoursView extends StatelessWidget { - CoursView({super.key}) { - // MAJ du ViewModel avec le nouveau cours séléctionné - CoursSelectionne coursSelectionne = CoursSelectionne.instance; - coursViewModel.loadContenu(coursSelectionne.cours); // #TODO : A mettre dans ListCours.dart - coursViewModel.setIndexPageVisite(coursSelectionne.cours); - } - - Widget? child; - - final coursViewModel = CoursViewModel(); - - Future changePage(BuildContext context) async { - CoursSelectionne coursSelectionne = CoursSelectionne.instance; - - int nbPageCours = await coursViewModel.getNombrePageDeContenu(coursSelectionne.cours); - int nbPageJeu = await coursViewModel.getNombrePageDeJeu(coursSelectionne.cours); - int page = coursViewModel.page; - - Widget nouvellePage = const Text("PB lors du chargement de la page de cours"); - if (page==0) { - nouvellePage = DescriptionView(cours: coursSelectionne.cours, coursViewModel: coursViewModel); - //print("Chargement de description"); - } else if (page<=nbPageCours) { - - nouvellePage = ContenuCoursView(cours: coursSelectionne.cours, selectedPageIndex: page - 1); - //print("Chargement de contenu"); - } else if (page<=nbPageCours + nbPageJeu) { - // Page jeu - nouvellePage = JeuQCMView(cours: coursSelectionne.cours, selectedPageIndex: page - nbPageCours - 1); - } else { - // Redirection vers la page de module - GoRouter.of(context).go('/module'); - } - - child = nouvellePage; - } - - @override - Widget build(BuildContext context) { - - return ListenableBuilder( - listenable: coursViewModel, // On écoute le changement de page du ViewModel - builder: (context, _) { - // Le futur Builder sert à attendre l'exécution de la méthode changePage() avant de build : - // Sans, la page à afficher (ie this.child) est récupéré après le build : - // Il n'est donc pas affiché - return FutureBuilder( - future: changePage(context), // Récupération de la bonne View à charger selon la page visitée du cours sélectionné - builder: (context, snapshot) { - - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 2, - title: FutureBuilder( // Permet d'attendre le calcul de progression - future: coursViewModel.getProgressionActuelle(CoursSelectionne.instance.cours), - builder: (context, snapshot) { - return HeaderWidget(cours: CoursSelectionne.instance.cours, progression: snapshot.data); - }), - centerTitle: false, - ), - body: child, - // La page de description n'a pas besoin du footer : Il change la page vu grâce à un bouton - bottomNavigationBar: child.runtimeType == DescriptionView ? null : - FooterWidget( - courseTitle: "Cours 1", - pageNumber: coursViewModel.page, - coursViewModel: coursViewModel, - ), - ); - } - ); - } - ); - - } -} - -class HeaderWidget extends StatelessWidget { - final Cours cours; - final double? progression; - - const HeaderWidget({ - Key? key, - required this.cours, - this.progression, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const SizedBox(width: 8), - // Titre - Text( - cours.titre, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - // Barre de progression - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: LinearProgressIndicator( - value: progression, // Progression à 50% - minHeight: 6, - color: Colors.teal, - backgroundColor: Colors.teal.withAlpha((255 * 0.2).round()), - ), - ), - ], - ); - - } - -} - -class FooterWidget extends StatelessWidget { - final String courseTitle; - final int pageNumber; - - final CoursViewModel coursViewModel; - - const FooterWidget({ - Key? key, - required this.courseTitle, - required this.pageNumber, - required this.coursViewModel, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.grey[200], - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Bouton flèche gauche - IconButton( - icon: const Icon(Icons.arrow_left, size: 28), - onPressed: () { - // Action pour aller à la page précédente - coursViewModel.changementPagePrecedente(); - }, - ), - // Texte de pagination - Text( - '$courseTitle : Page $pageNumber', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - // Bouton flèche droite - IconButton( - icon: const Icon(Icons.arrow_right, size: 28), - onPressed: () { - // Action pour aller à la page suivante - coursViewModel.changementPageSuivante(); - }, - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/ui/Cours/CoursViewModel.dart b/lib/ui/Cours/CoursViewModel.dart deleted file mode 100644 index e4aee3f..0000000 --- a/lib/ui/Cours/CoursViewModel.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:seriouse_game/logic/ProgressionUseCase.dart'; -import 'package:seriouse_game/repositories/QCM/QCMRepository.dart'; -import 'package:seriouse_game/repositories/mediaCoursRepository.dart'; -import 'package:seriouse_game/repositories/pageRepository.dart'; -import 'package:seriouse_game/models/cours.dart'; - -class CoursViewModel extends ChangeNotifier{ - CoursViewModel(); - - // Information de la page actuelle - // 0 : Page de description - // 1-nbPage : Page de contenu - // >nbPage : Page de jeu - int page = 0; - - final pageRepository = PageRepository(); - final qcmRepository = QCMRepository(); - final mediaCoursRepository = MediaCoursRepository(); - final progressionUseCase = ProgressionUseCase(); - - Future getNombrePageDeContenu(Cours cours) { - return pageRepository.getPagesByCourseId(cours.id!).then((lstPage) { - return lstPage.length; - }); - } - - Future getNombrePageDeJeu(Cours cours) { - return qcmRepository.getAllIdByCoursId(cours.id!).then((lstIdPageJeu) { - return lstIdPageJeu.length; - }); - } - - void setIndexPageVisite(Cours cours) { - // Attention la page 0 est la page de description, pas la 1ère page de contenu - pageRepository.getNbPageVisite(cours.id!).then( (indexPage) { - page = indexPage; - notifyListeners(); - }); - - } - - void changementPageSuivante() async { - page++; - await pageRepository.setPageVisite(page); - notifyListeners(); - } - - void changementPagePrecedente() { - if (page>0) { - page--; - notifyListeners(); - } - } - - Future getProgressionActuelle(Cours cours) async { - return await progressionUseCase.calculerProgressionActuelleCours(cours.id!, page)/100; - } - - Future loadContenu(Cours cours) async { - // Récupération des pages associées au cours - cours.pages = await pageRepository.getPagesByCourseId(cours.id!); - if (kDebugMode) { - print("Nombre de pages récupérées : \${cours.pages?.length}"); - } - - // Parcours des pages pour récupérer les médias associés - for (var page in cours.pages ?? []) { - page.medias = await mediaCoursRepository.getByPageId(page.id!); - if (kDebugMode) { - print("Nombre de médias pour la page \${page.id} : \${page.medias?.length}"); - } - } - } - -} - - diff --git a/lib/ui/Cours/cours_view.dart b/lib/ui/Cours/cours_view.dart new file mode 100644 index 0000000..750b2b3 --- /dev/null +++ b/lib/ui/Cours/cours_view.dart @@ -0,0 +1,260 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:factoscope/ui/Cours/cours_view_model.dart'; +import 'package:factoscope/ui/Description/description_view.dart'; +import 'package:factoscope/ui/Contenu/contenu_cours_view.dart'; +import 'package:factoscope/ui/QCM/jeu_qcm_view.dart'; +import 'package:factoscope/ui/cours_selectionne.dart'; + +import '../../models/cours.dart'; +import '../../repositories/QCM/fin_cours_view.dart'; +import '../../repositories/QCM/transition_qcm_view.dart'; +import '../../repositories/cours_repository.dart'; +import '../Cloze/cloze_page.dart'; + +class CoursView extends StatefulWidget { + final int coursId; + + const CoursView({super.key, required this.coursId}); + + @override + State createState() => _CoursViewState(); +} + +class _CoursViewState extends State { + final coursViewModel = CoursViewModel(); + Widget? child; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadCours(); + } + + Future _loadCours() async { + try { + final coursRepository = CoursRepository(); + final loadedCours = await coursRepository.getById(widget.coursId); + if (loadedCours != null) { + CoursSelectionne.instance.setCours(loadedCours); + await coursViewModel.loadContenu(loadedCours); + await coursViewModel.setIndexPageVisite(loadedCours); + } + setState(() { + isLoading = false; + }); + } catch (e) { + if (kDebugMode) { + print("Erreur lors du chargement du cours: $e"); + } + setState(() { + isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + if (isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + return ListenableBuilder( + listenable: coursViewModel, + builder: (context, _) { + return _buildCoursView(context); + }, + ); + } + + Widget _buildCoursView(BuildContext context) { + CoursSelectionne coursSelectionne = CoursSelectionne.instance; + int nbPageCours = coursSelectionne.cours.pages?.length ?? 0; + int currentPage = coursViewModel.page; + + return FutureBuilder>( + future: Future.wait([ + coursViewModel.getNombrePageQCM(coursSelectionne.cours), + coursViewModel.getNombrePageCloze(coursSelectionne.cours), + coursViewModel.getTypeJeu(coursSelectionne.cours.id!), // ← ajout + ]), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + int nbQCM = snapshot.data![0] as int; + int nbCloze = snapshot.data![1] as int; + String typeJeu = snapshot.data![2] as String; + + int transitionPage = nbPageCours + 1; + int firstQCMPage = transitionPage + 1; + int lastQCMPage = firstQCMPage + nbQCM - 1; + int firstClozePage = lastQCMPage + 1; + int lastClozePage = firstClozePage + nbCloze - 1; + int finPage = lastClozePage + 1; + + Widget nouvellePage; + + if (currentPage == 0) { + nouvellePage = DescriptionView( + cours: coursSelectionne.cours, + coursViewModel: coursViewModel, + ); + } else if (currentPage <= nbPageCours) { + nouvellePage = ContenuCoursView( + cours: coursSelectionne.cours, + selectedPageIndex: currentPage - 1, + ); + } else if (currentPage == transitionPage) { + nouvellePage = TransitionQCMView( + cours: coursSelectionne.cours, + coursViewModel: coursViewModel, + ); + } else if (currentPage >= firstQCMPage && currentPage <= lastQCMPage) { + if (typeJeu == 'qcm') { + nouvellePage = JeuQCMView(cours: coursSelectionne.cours); + } else if (typeJeu == 'cloze') { + int clozeIndex = currentPage - firstQCMPage; + nouvellePage = ClozePage( + coursId: coursSelectionne.cours.id!, + key: ValueKey('cloze_$clozeIndex'), + ); + } else { + nouvellePage = const Center(child: Text("Aucun jeu disponible")); + } + } else if (currentPage >= firstClozePage && currentPage <= lastClozePage) { + if (typeJeu == 'cloze') { + int clozeIndex = currentPage - firstClozePage; + nouvellePage = ClozePage( + coursId: coursSelectionne.cours.id!, + key: ValueKey('cloze_$clozeIndex'), + ); + } else { + nouvellePage = const Center(child: Text("Aucun jeu disponible")); + } + } else if (currentPage == finPage) { + nouvellePage = FinCoursView(cours: coursSelectionne.cours); + } else { + nouvellePage = const Center(child: Text("Page introuvable")); + } + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 2, + title: FutureBuilder( + future: coursViewModel.getProgressionActuelle(coursSelectionne.cours), + builder: (context, snapshot) { + return HeaderWidget( + cours: coursSelectionne.cours, + progression: snapshot.data, + ); + }, + ), + centerTitle: false, + ), + body: nouvellePage, + bottomNavigationBar: (nouvellePage.runtimeType != DescriptionView && + nouvellePage.runtimeType != FinCoursView) + ? FooterWidget( + courseTitle: coursSelectionne.cours.titre, + pageNumber: currentPage, + coursViewModel: coursViewModel, + cours: coursSelectionne.cours, + ) + : null, + ); + }, + ); + } +} + +class HeaderWidget extends StatelessWidget { + final Cours cours; + final double? progression; + + const HeaderWidget({ + super.key, + required this.cours, + this.progression, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(width: 8), + Text( + cours.titre, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + if (progression != null) + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progression, + minHeight: 6, + color: Colors.teal, + backgroundColor: Colors.teal.withValues(alpha: 0.2), + ), + ), + ], + ); + } +} + +class FooterWidget extends StatelessWidget { + final String courseTitle; + final int pageNumber; + final CoursViewModel coursViewModel; + final Cours cours; + + const FooterWidget({ + super.key, + required this.courseTitle, + required this.pageNumber, + required this.coursViewModel, + required this.cours, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.grey[200], + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.arrow_left, size: 28), + onPressed: () { + coursViewModel.changementPagePrecedente(); + }, + ), + Text( + '$courseTitle : Page $pageNumber', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + IconButton( + icon: const Icon(Icons.arrow_right, size: 28), + onPressed: () { + coursViewModel.changementPageSuivante(cours); + }, + ), + ], + ), + ); + } +} diff --git a/lib/ui/Cours/cours_view_model.dart b/lib/ui/Cours/cours_view_model.dart new file mode 100644 index 0000000..895b75a --- /dev/null +++ b/lib/ui/Cours/cours_view_model.dart @@ -0,0 +1,108 @@ +import 'package:flutter/foundation.dart'; +import 'package:factoscope/logic/progression_use_case.dart'; +import 'package:factoscope/repositories/QCM/qcm_repository.dart'; +import 'package:factoscope/repositories/page_repository.dart'; +import 'package:factoscope/models/cours.dart'; + +import '../../database_helper.dart'; +import '../../repositories/Cloze/cloze_repository.dart'; + +class CoursViewModel extends ChangeNotifier { + CoursViewModel(); + + // Information de la page actuelle + // 0 : Page de description + // 1-nbPage : Page de contenu + // >nbPage : Page de jeu + int page = 0; + + final pageRepository = PageRepository(); + final qcmRepository = QCMRepository(); + final progressionUseCase = ProgressionUseCase(); + + Future getNombrePageDeContenu(Cours cours) async { + final lstPage = await pageRepository.getPagesByCourseId(cours.id!); + return lstPage.length; + } + + Future getNombrePageQCM(Cours cours) async { + int nbQCM = await qcmRepository.getAllIdByCoursId(cours.id!).then((lstIdPageJeu) => lstIdPageJeu.length); + return nbQCM; + } + + Future getNombrePageCloze(Cours cours) async { + int nbCloze = await progressionUseCase.getNombrePageDeCloze(cours); + return nbCloze; + } + + Future setIndexPageVisite(Cours cours) async { + final indexPage = await pageRepository.getNbPageVisite(cours.id!); + page = indexPage; + notifyListeners(); + } + + Future changementPageSuivante(Cours cours) async { + final nbPages = await getNombrePageDeContenu(cours); + int nbJeux = await getNombrePageQCM(cours); + nbJeux += await getNombrePageCloze(cours); + // Total: description(0) + pages(nbPages) + transition(1) + qcm(nbJeux) + fin(1) + final totalPages = + nbPages + nbJeux + 2; // +2 c'est pour la transition et la page de fin + + if (page < totalPages) { + page++; + + // Marquer la page comme visitée seulement si c'est une page de contenu (pas description, pas transition, pas jeu) + if (page > 0 && page <= nbPages) { + final pages = await pageRepository.getPagesByCourseId(cours.id!); + if (page - 1 < pages.length) { + await pageRepository.setPageVisite(pages[page - 1].id!); + } + } + + notifyListeners(); + } + } + + void changementPagePrecedente() { + if (page > 0) { + page--; + notifyListeners(); + } + } + + final clozeRepository = ClozeRepository(); + + Future getProgressionActuelle(Cours cours) async { + return await progressionUseCase.calculerProgressionActuelleCours( + cours.id!, page) / + 100; + } + + Future loadContenu(Cours cours) async { + // Récupération des pages avec leurs médias déjà parsés + cours.pages = await pageRepository.getPagesByCourseId(cours.id!); + } + + Future getTypeJeu(int coursId) async { + final db = await DatabaseHelper.instance.database; + + final qcmResult = await db.query( + 'qcm', + where: 'id_cours = ?', + whereArgs: [coursId], + limit: 1, + ); + if (qcmResult.isNotEmpty) return 'qcm'; + + final clozeResult = await db.query( + 'Cloze', + where: 'idCours = ?', + whereArgs: [coursId], + limit: 1, + ); + if (clozeResult.isNotEmpty) return 'cloze'; + + return 'aucun'; + } +} diff --git a/lib/ui/CoursSelectionne.dart b/lib/ui/CoursSelectionne.dart deleted file mode 100644 index 0405530..0000000 --- a/lib/ui/CoursSelectionne.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:seriouse_game/models/cours.dart'; - -class CoursSelectionne { - CoursSelectionne._privateConstructor(); - - static final CoursSelectionne _instance = CoursSelectionne._privateConstructor(); - - static CoursSelectionne get instance => _instance; - - Cours cours = Cours(idModule: 0, titre: "UTILISE POUR INIT LE SINGLETON COURSSELECTIONNE", contenu: "UTILISE POUR INIT LE SINGLETON COURSSELECTIONNE"); - - void setCours(Cours cours) { - this.cours = cours; - } -} \ No newline at end of file diff --git a/lib/ui/Description/DescriptionView.dart b/lib/ui/Description/DescriptionView.dart deleted file mode 100644 index ec18f9f..0000000 --- a/lib/ui/Description/DescriptionView.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; -import 'package:seriouse_game/services/coursService.dart'; -import 'package:seriouse_game/models/cours.dart'; - -import 'package:seriouse_game/ui/Cours/CoursViewModel.dart'; - -class DescriptionView extends StatelessWidget { - const DescriptionView({Key? key, required this.cours, required this.coursViewModel}) : super(key: key); - - final CoursViewModel coursViewModel; // Permet de changer la page de cours (utilisé pour le bouton "Commencer le cours") - final Cours cours; - - @override - Widget build(BuildContext context) { - final CoursService coursService = GetIt.I(); - - return Scaffold( - appBar: AppBar( - title: const Text("Description du Cours"), - ), - body: FutureBuilder( - future: coursService.getCoursWithObjectifs(cours.id!), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError || !snapshot.hasData) { - return const Center(child: Text("Aucun cours trouvé.")); - } - - final cours = snapshot.data!; - - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Image du cours - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.asset( - 'lib/data/AppData/goals.png', // #TODO: Remplace par ton image - fit: BoxFit.cover, - height: 200, - width: double.infinity, - ), - ), - const SizedBox(height: 24), - // Titre du cours - Text( - cours.titre, - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - // Description du cours - Text( - cours.contenu, - style: const TextStyle(fontSize: 18), - ), - const SizedBox(height: 24), - // Section des objectifs - const Text( - "Objectifs :", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - // Liste des objectifs - if (cours.objectifs != null && cours.objectifs!.isNotEmpty) - ...cours.objectifs!.map( - (objectif) => Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.check_circle, color: Colors.green, size: 20), - const SizedBox(width: 8), - Expanded( - child: Text( - objectif.description, - style: const TextStyle(fontSize: 16), - ), - ), - ], - ), - ), - ), - const SizedBox(height: 32), - // Bouton pour commencer le cours - Center( - child: ElevatedButton( - onPressed: () { - // Action pour démarrer le cours - coursViewModel.changementPageSuivante(); - }, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - backgroundColor: const Color.fromRGBO(252, 179, 48, 1) - ), - child: const Text( - "Commencer le cours", - style: TextStyle( - fontSize: 18, - color: Colors.white - ), - - ), - ), - ), - ], - ), - ), - ); - }, - ), - ); - } -} \ No newline at end of file diff --git a/lib/ui/Description/description_view.dart b/lib/ui/Description/description_view.dart new file mode 100644 index 0000000..84733ea --- /dev/null +++ b/lib/ui/Description/description_view.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/ui/Cours/cours_view_model.dart'; + +class DescriptionView extends StatelessWidget { + const DescriptionView({ + super.key, + required this.cours, + required this.coursViewModel, + }); + + final CoursViewModel coursViewModel; + final Cours cours; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image du cours + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + 'lib/assets/goals.png', + fit: BoxFit.cover, + height: 200, + width: double.infinity, + ), + ), + const SizedBox(height: 24), + // Titre du cours + Text( + cours.titre, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + // Description du cours + Text( + cours.contenu, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(height: 24), + // ✅ Section objectifs supprimée car n'existe plus + const SizedBox(height: 32), + // Bouton pour commencer le cours + Center( + child: ElevatedButton( + onPressed: () { + coursViewModel.changementPageSuivante(cours); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + backgroundColor: const Color.fromRGBO(252, 179, 48, 1), + ), + child: const Text( + "Commencer le cours", + style: TextStyle(fontSize: 18, color: Colors.white), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/LaunchScreen/LaunchScreenView.dart b/lib/ui/LaunchScreen/launch_screen_view.dart similarity index 64% rename from lib/ui/LaunchScreen/LaunchScreenView.dart rename to lib/ui/LaunchScreen/launch_screen_view.dart index d57d216..ccf098c 100644 --- a/lib/ui/LaunchScreen/LaunchScreenView.dart +++ b/lib/ui/LaunchScreen/launch_screen_view.dart @@ -6,10 +6,11 @@ class LaunchScreenView extends StatefulWidget { const LaunchScreenView({super.key}); @override - _LaunchScreenViewState createState() => _LaunchScreenViewState(); + LaunchScreenViewState createState() => LaunchScreenViewState(); } -class _LaunchScreenViewState extends State with SingleTickerProviderStateMixin { +class LaunchScreenViewState extends State + with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _opacityAnimation; late Animation _offsetAnimation; @@ -40,9 +41,10 @@ class _LaunchScreenViewState extends State with SingleTickerPr )); Future.delayed(const Duration(seconds: 2), () { - _controller.forward(); + if (mounted) { + _controller.forward(); + } }); - } @override @@ -55,32 +57,31 @@ class _LaunchScreenViewState extends State with SingleTickerPr Widget build(BuildContext context) { try { return Scaffold( - body: FadeTransition( - opacity: _opacityAnimation, - child: Container( - color: const Color.fromRGBO(252, 179, 48, 1), - child: Center( - child: SlideTransition( - position: _offsetAnimation, - child: SizedBox( - width: 250, - height: 250, - child: Image.asset( - 'lib/data/AppData/CharteFactoscope/logo-factoscope.png', - fit: BoxFit.contain, + body: FadeTransition( + opacity: _opacityAnimation, + child: Container( + color: const Color.fromRGBO(252, 179, 48, 1), + child: Center( + child: SlideTransition( + position: _offsetAnimation, + child: SizedBox( + width: 250, + height: 250, + child: Image.asset( + 'lib/assets/logo-factoscope.png', + fit: BoxFit.contain, + ), ), ), ), ), ), - ), - ); + ); } catch (e) { if (kDebugMode) { print("Erreur lors du chargement du QCM : $e"); } return const Text("aa"); } - } } diff --git a/lib/ui/ListCoursView.dart b/lib/ui/ListCoursView.dart deleted file mode 100644 index 26b077e..0000000 --- a/lib/ui/ListCoursView.dart +++ /dev/null @@ -1,279 +0,0 @@ -// ignore_for_file: must_be_immutable - -import 'package:flutter/material.dart'; -import 'package:seriouse_game/models/ListCoursViewModel.dart'; -import 'package:seriouse_game/models/mediaCours.dart'; -import 'package:seriouse_game/ui/Contenu/WidgetContenu/ContenuImageWidget.dart'; -import 'package:seriouse_game/ui/Cours/CoursView.dart'; -import 'package:seriouse_game/ui/CoursSelectionne.dart'; - -import '../models/cours.dart'; -import '../models/module.dart'; -import 'ModuleSelectionne.dart'; - -import 'package:go_router/go_router.dart'; - -class ListCoursView extends StatefulWidget { - - ListCoursViewModel listCours = ListCoursViewModel(); - ModuleSelectionne moduleSelectionne = ModuleSelectionne(); - - ListCoursView({super.key}); - - @override - State createState() => ListCoursViewState(listCours,moduleSelectionne); - -} - -class ListCoursViewState extends State{ - - late ListCoursViewModel listCours; - late ModuleSelectionne moduleSelectionne; - - ListCoursViewState(this.listCours,this.moduleSelectionne); - - - @override - Widget build(BuildContext context) { - - return ListenableBuilder( - - //On écoute moduleSelectionne pour changer l'interface si le module change. - listenable: moduleSelectionne, - builder: (context, child) { - - - int size = 0; - - // Récupération du module sélectionné - Module module = moduleSelectionne.moduleSelectionne; - - // Chargement des cours associés dans le module - listCours.recupererCours(module.id); - - //récupération de la taille de la liste du module - size = moduleSelectionne.coursDuModule.length; - - //Widget - return ListenableBuilder( - - //On écoute l'état de listCours pour changer l'affichage si cette liste change - listenable: listCours, - - builder: (context, child) { - - - return Column( - children: [ - - //Appel du widget affichant le titre - FutureBuilder( // Permet d'attendre le calcul de progression - future: ListCoursViewModel().getProgressionModule(module), - builder: (context, snapshot) { - return Container( - //On veut ajouter une marge ici car on ne peut pas en ajouter directement dans moduleHeader sans l'augmenter dans la liste des modules - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0), - child: moduleHeader(module, snapshot.data)); - } ), - - - //Affichage des informations sur le module (description, cours, ...) dans une liste - - Expanded( - child: - ListView.builder( - - - //On initialise le nombre de widgets à affiché par celui de la liste de cours - itemCount: size+1, - - itemBuilder: (context, index) { - - //Si l'index est à 0 : on affiche la description du module, sinon on affiche le cours index-1 - if(index==0){ - - return Align( - alignment: Alignment.centerLeft, - child : - Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - child: - - const Text( - "Description et Objectif du Module", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - textAlign: TextAlign.left, - overflow: TextOverflow.ellipsis, - - ), - ), - - Container( - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 8.0), - child: - //Texte de la description - Text(module.description, - style: const TextStyle( - fontSize: 18, - color: Colors.black, - ), - textAlign: TextAlign.left, - - ) - ), - ], - ), - ); - - //Affichage de la liste des cours du module - }else{ - //On extraie pour chaque élément de la liste le cours dans item - final item = moduleSelectionne.coursDuModule[index-1]; - - //On build le widget à partir du titre d'item - return listItem(item, context); - } - - }, - ), - - ), - ], - );// Widgets de liste avec données - - } - ); - } - ); - } - - -} - -//Widget correspondant au titre du module - SizedBox moduleHeader(Module module, double? progress){ - //progress : Valeur de la progression entre 0 et 1 - //module : module dont on veut afficher le header - - //Model utilisé pour récupérer l'image du module à afficher dans le header. - MediaCours media = MediaCours(idPage: 1, ordre: 1, url: module.urlImg, type: "image"); - - return SizedBox( - child : Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), - //Gestion de l'espace entre l'exterieur du widget et les widgets adjacents - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - //Décoration de la bordure - decoration: BoxDecoration( - //Gestion de l'angle de la bordure - borderRadius:BorderRadius.circular(12), - //Couleur interne et externe de la boite - color: const Color.fromARGB(255, 236, 187, 139),), - child : Row( - children: [ - - //Image à load. Utilise le model media contenant l'url de l'image du model. - ContenuImageWidget(media: media, width: 80, height: 80,), - - const Spacer(), - - Column( - - //Titre du module - children: [ - - - Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), - //Gestion de l'espace entre l'exterieur du widget et les widgets adjacents - margin: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), - child: Text(module.titre, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - overflow: TextOverflow.clip, - ), - ), - - //Affichage de la barre de progression du module - //SizedBox est nécessaire car LinearProgressIndicator doit être contenue dans un objet de largeur définie - //Column n'ayant pas de largeur max, on ne peut pas mettre le wwidget directement dedans - SizedBox( - //Longueur max du widget - width: 200, - child: - //Container pour des bords plus arrondis - ClipRRect( - borderRadius: BorderRadius.circular(4), - //Barre de progression du module - child: LinearProgressIndicator( - value:progress, //On utilise progress pour définir le remplissage - minHeight: 6, - color: const Color.fromARGB(255, 90, 230, 220), - backgroundColor: const Color.fromARGB(255, 175, 240, 235), - ) - - ), - ) - - - ], - ), - - const Spacer() - ], - ), - ) - ); - } - //Widget permettant d'afficher et de sélectionner un cours de la liste - SizedBox listItem(Cours cours, BuildContext context){ - - return SizedBox( - child : - //On utilise Inkwell pour transformer notre container en bouton - InkWell( - child: - Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), - //Gestion de l'espace entre l'exterieur du widget et les widgets adjacents - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - //On utilise le header de CoursView - child: FutureBuilder( // Permet d'attendre le calcul de progression - future: ListCoursViewModel().getProgressionCours(cours), - builder: (context, snapshot) { - return Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0), - //Décoration de la bordure - decoration: BoxDecoration( - //Gestion de l'angle de la bordure - borderRadius:BorderRadius.circular(12), - //Couleur interne et externe de la boite - color: const Color.fromARGB(255, 235, 235, 235),), - - child: - HeaderWidget(cours: cours, progression: snapshot.data)); - }), - ), - //Méthode pour se aller au cours - onTap: (){ - - CoursSelectionne.instance.cours = cours; - GoRouter.of(context).go('/cours'); - } - - ), - ); - } \ No newline at end of file diff --git a/lib/ui/ListModuleView.dart b/lib/ui/ListModuleView.dart deleted file mode 100644 index c28df42..0000000 --- a/lib/ui/ListModuleView.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:seriouse_game/models/ListCoursViewModel.dart'; -import 'package:seriouse_game/models/module.dart'; -import 'package:seriouse_game/ui/ListCoursView.dart'; -import 'package:seriouse_game/ui/ModuleSelectionne.dart'; - -import '../models/ListModuleViewModel.dart'; - -import 'package:go_router/go_router.dart'; - -//Widget de la page d'accueil/Liste des modules -class ListModulesView extends StatefulWidget { - - const ListModulesView({super.key}); - - @override - State createState() => _ListModulesViewState(); -} - -//State du widget d'affichage de la liste des modules( page d'accueil ) -class _ListModulesViewState extends State { - - //Objet ayant accès à la liste des modules de l'application - ListModuleViewModel listModuleViewModel = ListModuleViewModel(); - - @override - Widget build(BuildContext context) { - - //On apelle la méthode permettant de récupérer la liste des modules - listModuleViewModel.recupererModule(); - - return ListenableBuilder( - - //On écoute les changements dans la liste des modules - listenable: listModuleViewModel, - - builder: (context,child) { - - return Column( - - - children: [ - - //Affiche Overview en gras à gauche - Align( - alignment: Alignment.centerLeft, - child : - Container( - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - child: - const Text( - "Accueil", - style: TextStyle( - fontSize: 35, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - textAlign: TextAlign.left, - - ), - ), - ), - - headerAvancement(), - - //Affiche Modules en gras à gauche - Align( - alignment: Alignment.centerLeft, - child : - Container( - margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - child: - const Text( - "Cours :", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - textAlign: TextAlign.left, - - ), - ), - ), - - //Affichage de la liste des modules - - Expanded( - child: - ListView.builder( - - //On initialise le nombre de widgets à affiché par celui de la liste des modules - itemCount: listModuleViewModel.listModule.length, - - itemBuilder: (context, index) { - //On extraie pour chaque élément de la liste le module dans item - final item = listModuleViewModel.listModule[index]; - - //On build le widget à partir d'item - return listModuleItem(item, context); - }, - ), - ), - - ], - ); - } - ); - - } -} - -//Widget représentant l'header d'un module dans la liste des modules. -SizedBox listModuleItem(Module item, BuildContext context) { - - return SizedBox( - child : - //On créer un stack pour avoir un bouton de teléchargement sur le header du module - Stack( - alignment: Alignment.center, - children: [ - - //On utilise Inkwell pour transformer notre container en bouton - InkWell( - child: FutureBuilder( // Permet d'attendre le calcul de progression - future: ListCoursViewModel().getProgressionModule(item), - builder: (context, snapshot) { - return moduleHeader(item, snapshot.data); - } ), - //Méthode pour aller au module - onTap: (){ - - ModuleSelectionne.instance.moduleSelectionne = item; - GoRouter.of(context).go('/module'); - } - ), - - //Bouton de téléchargement - Align( - alignment: Alignment.topRight, //On aligne le bouton à doite - child: - IconButton( - //Méthode permettant de télécharger le module - onPressed: (){ - - //Ajouter méthode asset delivery - - }, - //Icone de telechargement - icon: const Icon( - Icons.download, - color: Colors.white, - size: 20, - ) - ), - ), - ], - ) - ); -} - -//Header affichant l'avancement dans le cours de l'utilisateur -SizedBox headerAvancement(){ - - return SizedBox( - child: Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), - //Gestion de l'espace entre l'exterieur du widget et les widgets adjacents - margin: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0), - //Décoration de la bordure - decoration: BoxDecoration( - //Gestion de l'angle de la bordure - borderRadius:BorderRadius.circular(12), - color: const Color.fromARGB(255, 219, 218, 215),), - child: Row( - children: [ - Container( - //Gestion de l'espace entre le contenu et la bordure interieure du widget - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), - //Gestion de l'espace entre l'exterieur du widget et les widgets adjacents - margin: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), - //Colonne contenant le texte du header - child: const Column( - children: [ - //Titre - Text("Votre Avancement", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, - ),), - //Message d'encouragement - Text("Tout est possible à qui "), - Text("rêve, ose, travaille "), - Text("et n'abandonne jamais.") - ], - ), - ), - - const Spacer(), - - //On utilise un stack pour mettre le pourcentage au dessus d'une icône - Stack( - alignment: Alignment.center, - children: [ - - //Icône : Attention ce n'est pas la bonne icône pour l'instant - const Icon( - Icons.bookmark, - size: 140, - color: Color.fromARGB(255, 3, 47, 122),), - - //Affichage du pourcentage d'avancement - FutureBuilder( // Permet d'attendre le calcul de progression - future: ListModuleViewModel().getProgressionGlobale(), - builder: (context, snapshot) { - String progress = ""; - if (snapshot.hasData) { - progress = snapshot.data.toString(); - } - - return Text("$progress%", - style: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - color: Colors.white, - ),); - } ), - - - ], - ), - ], - ) - - - ), - ); - -} \ No newline at end of file diff --git a/lib/ui/QCM/JeuQCMView.dart b/lib/ui/QCM/JeuQCMView.dart deleted file mode 100644 index ccb45e1..0000000 --- a/lib/ui/QCM/JeuQCMView.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:seriouse_game/models/QCM/question.dart'; -import 'package:seriouse_game/models/QCM/reponse.dart'; -import 'package:seriouse_game/models/cours.dart'; -import 'JeuQCMViewModel.dart'; - -class JeuQCMView extends StatefulWidget { - final Cours cours; - final int selectedPageIndex; - - const JeuQCMView({super.key, required this.cours, required this.selectedPageIndex}); - - @override - _JeuQCMViewState createState() => _JeuQCMViewState(); -} - -class _JeuQCMViewState extends State { - int? _selectedAnswer; - bool _validated = false; - - // Ajout de la logique pour suivre si on est sur la question suivante - @override - void didUpdateWidget(covariant JeuQCMView oldWidget) { - super.didUpdateWidget(oldWidget); - - // Si la page sélectionnée change, réinitialiser l'état - if (oldWidget.selectedPageIndex != widget.selectedPageIndex) { - setState(() { - _selectedAnswer = null; - _validated = false; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("Jeu QCM")), - body: FutureBuilder>( - future: JeuQCMViewModel().recupererQCM(widget.cours, widget.selectedPageIndex), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return const Center(child: Text("Erreur lors du chargement")); - } else if (!snapshot.hasData) { - return const Center(child: Text("Aucune donnée disponible")); - } - - var data = snapshot.data as Map; - - Question question = data["question"]; - String? questionText; - if (question.type == "text") { - questionText = question.text; - } else if (question.type == "image") { - questionText = question.imageUrl; - } else { - throw Exception("Format question non respecte : Image ou texte "); - } - - List reponses = data["options"]; - List reponseText; - if (reponses.first.imageUrl == null && reponses.first.text != null) { - reponseText = reponses.map((r) => r.text).toList(); - } else if (reponses.first.imageUrl != null && reponses.first.text == null) { - reponseText = reponses.map((r) => r.imageUrl).toList(); - } else { - throw Exception("Format reponse non respecter : Image ou texte "); - } - - int correctAnswer = data["correctAnswer"]; - - return Column( - children: [ - _buildQuestionWidget(questionText), - ...List.generate(reponseText.length, (index) => _buildAnswerWidget(reponseText[index], index + 1, correctAnswer)), - ElevatedButton( - onPressed: _selectedAnswer == null - ? null - : () { - setState(() { - _validated = true; - }); - }, - child: const Text("Valider"), - ), - ], - ); - }, - ), - ); - } - - Widget _buildQuestionWidget(dynamic question) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: question is String - ? Text(question, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)) - : Image.network(question), // Remplace par Image.asset si fichiers locaux - ); - } - - Widget _buildAnswerWidget(dynamic answer, int index, int correctAnswer) { - Color? color; - if (_validated) { - if (index == correctAnswer) { - color = Colors.green; - } else if (index == _selectedAnswer) { - color = Colors.red; - } - } - - return ListTile( - title: answer is String ? Text(answer) : Image.network(answer), - leading: Radio( - value: index, - groupValue: _selectedAnswer, - onChanged: _validated - ? null - : (int? value) { - setState(() { - _selectedAnswer = value; - }); - }, - ), - tileColor: color, - ); - } -} diff --git a/lib/ui/QCM/JeuQCMViewModel.dart b/lib/ui/QCM/JeuQCMViewModel.dart deleted file mode 100644 index d47c69e..0000000 --- a/lib/ui/QCM/JeuQCMViewModel.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:seriouse_game/models/QCM/qcm.dart'; -import 'package:seriouse_game/models/QCM/question.dart'; -import 'package:seriouse_game/models/QCM/reponse.dart'; -import 'package:seriouse_game/models/cours.dart'; -import 'package:seriouse_game/repositories/QCM/QCMRepository.dart'; - -class JeuQCMViewModel { - Future> recupererQCM(Cours cours, int selectedPageIndex) async { - try { - final qcmRepo = QCMRepository(); - List idQCMList = await qcmRepo.getAllIdByCoursId(cours.id!); - QCM? qcm = await qcmRepo.getById(idQCMList[selectedPageIndex]); - - Question? question; - List? reponses = []; - int? solution; - - if (qcm == null || qcm.question == null || qcm.reponses == null) { - throw Exception("QCM incomplet ou invalide"); - } - - question = qcm.question; - reponses = qcm.reponses; - solution = qcm.numSolution; - - return { - "question": question, - "options": reponses, - "correctAnswer": solution, - }; - - } catch (e) { - if (kDebugMode) { - print("Erreur lors du chargement du QCM : $e"); - } - return {}; - } - } -} - - diff --git a/lib/ui/QCM/jeu_qcm_view.dart b/lib/ui/QCM/jeu_qcm_view.dart new file mode 100644 index 0000000..10ad0e6 --- /dev/null +++ b/lib/ui/QCM/jeu_qcm_view.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'jeu_qcm_view_model.dart'; +import 'package:factoscope/models/cours.dart'; + +class JeuQCMView extends StatelessWidget { + final Cours cours; + + const JeuQCMView({super.key, required this.cours}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => JeuQCMViewModel()..chargerQCM(cours), + child: Consumer( + builder: (context, vm, child) { + if (vm.isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + if (vm.hasError || vm.controller == null) { + return const Scaffold( + body: Center(child: Text("Impossible de charger le QCM")), + ); + } + + if (vm.isFinished) { + return _buildResultPage(context, vm); + } + + return _buildQuestionPage(context, vm); + }, + ), + ); + } + + // --- PAGE QUESTION --- + Widget _buildQuestionPage(BuildContext context, JeuQCMViewModel vm) { + return Scaffold( + appBar: AppBar( + title: const Text("QCM"), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Question ${vm.currentIndex + 1} / ${vm.totalQuestions}", + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + + const SizedBox(height: 20), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + vm.questionText, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + ), + + const SizedBox(height: 20), + + if (vm.isCorrect != null) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text( + vm.isCorrect! ? "Bonne réponse !" : "Mauvaise réponse...", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: vm.isCorrect! ? Colors.green : Colors.red, + ), + ), + ), + + Expanded( + child: ListView.builder( + itemCount: vm.options.length, + itemBuilder: (context, i) { + final isSelected = vm.selectedAnswer == i; + + Color tileColor = Colors.white; + + if (vm.isCorrect != null && isSelected) { + tileColor = vm.isCorrect! + ? Colors.green.shade200 + : Colors.red.shade200; + } + + return Card( + color: tileColor, + elevation: 2, + margin: const EdgeInsets.symmetric(vertical: 8), + child: ListTile( + title: Text( + vm.options[i], + style: const TextStyle(fontSize: 16), + ), + onTap: () { + if (vm.selectedAnswer == null) { + vm.selectAnswer(i); + } + }, + ), + ); + }, + ), + ), + + const SizedBox(height: 10), + + // --- BOUTONS JAUNES IDENTIQUES À TON IMAGE --- + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // --- BOUTON PRECEDENT --- + GestureDetector( + onTap: vm.currentIndex > 0 ? vm.previous : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14), + decoration: BoxDecoration( + color: vm.currentIndex > 0 + ? const Color(0xFFFFD54F) + : const Color(0xFFFFECB3), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.orange.shade300), + ), + child: const Row( + children: [ + Icon(Icons.arrow_back, color: Colors.black87), + SizedBox(width: 8), + Text( + "Précédent", + style: TextStyle( + color: Colors.black87, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + + // --- BOUTON SUIVANT --- + GestureDetector( + onTap: vm.selectedAnswer != null ? vm.next : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14), + decoration: BoxDecoration( + color: vm.selectedAnswer != null + ? const Color(0xFFFFD54F) + : const Color(0xFFFFECB3), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.orange.shade300), + ), + child: Row( + children: [ + Text( + vm.currentIndex == vm.totalQuestions - 1 + ? "Terminer" + : "Suivant", + style: const TextStyle( + color: Colors.black87, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward, color: Colors.black87), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + // --- PAGE RESULTAT --- + Widget _buildResultPage(BuildContext context, JeuQCMViewModel vm) { + final score = vm.getScore(); + + return Scaffold( + appBar: AppBar(title: const Text("Résultat")), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Votre score", + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + Text( + "$score / ${vm.totalQuestions}", + style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 30), + + ElevatedButton( + onPressed: vm.restart, + child: const Text("Recommencer"), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/QCM/jeu_qcm_view_model.dart b/lib/ui/QCM/jeu_qcm_view_model.dart new file mode 100644 index 0000000..f1ad5c8 --- /dev/null +++ b/lib/ui/QCM/jeu_qcm_view_model.dart @@ -0,0 +1,78 @@ +import 'package:flutter/foundation.dart'; +import 'package:factoscope/models/QCM/qcm.dart'; +import 'package:factoscope/models/cours.dart'; + +import 'package:factoscope/repositories/QCM/qcm_repository.dart'; +import 'package:factoscope/repositories/QCM/qcm_controller.dart'; + +class JeuQCMViewModel extends ChangeNotifier { + final QCMRepository _repo = QCMRepository(); + + QCMController? controller; + bool isLoading = true; + bool hasError = false; + + Future chargerQCM(Cours cours) async { + try { + isLoading = true; + notifyListeners(); + + List ids = await _repo.getAllIdByCoursId(cours.id!); + List qcms = []; + + for (int id in ids) { + QCM? q = await _repo.getById(id); + if (q != null) qcms.add(q); + } + + if (qcms.isEmpty) { + throw Exception("Aucun QCM trouvé"); + } + + controller = QCMController(qcms); + controller!.start(); + + isLoading = false; + notifyListeners(); + } catch (e) { + hasError = true; + isLoading = false; + notifyListeners(); + } + } + + // --- Exposition des données à la vue --- + String get questionText => controller!.currentQuestion.question; + + List get options => controller!.currentQuestion.getReponses(); + + int? get selectedAnswer => controller!.selectedAnswer; + int get currentIndex => controller!.currentIndex; + int get totalQuestions => controller!.qcmList.length; + bool get isFinished => controller!.state == QCMState.finished; + + bool? get isCorrect => controller!.isCorrect; + + // --- Actions utilisateur --- + void selectAnswer(int index) { + controller!.selectAnswer(index); + notifyListeners(); + } + + void next() { + controller!.next(); + notifyListeners(); + } + + void previous() { + controller!.previous(); + notifyListeners(); + } + + int getScore() => controller!.getScore(); + + void restart() { + controller!.restart(); + notifyListeners(); + } +} diff --git a/lib/ui/QCM/page_succes_qcm.dart b/lib/ui/QCM/page_succes_qcm.dart new file mode 100644 index 0000000..5ed0261 --- /dev/null +++ b/lib/ui/QCM/page_succes_qcm.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +class PageSuccesQCM extends StatefulWidget { + const PageSuccesQCM({super.key}); + + @override + State createState() => _PageSuccesQCMState(); +} + +class _PageSuccesQCMState extends State { + final TextEditingController nomController = TextEditingController(); + final TextEditingController prenomController = TextEditingController(); + final TextEditingController dateController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Certification"), + centerTitle: true, + backgroundColor: Colors.white, + elevation: 2, + ), + body: Padding( + padding: const EdgeInsets.all(20), + child: ListView( + children: [ + const Text( + "Félicitations ! 🎉", + style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + + const Text( + "Vous avez obtenu 100% au QCM officiel.\nVeuillez renseigner vos informations pour générer votre certificat.", + style: TextStyle(fontSize: 16), + ), + + const SizedBox(height: 30), + + _buildInput("Nom", nomController), + _buildInput("Prénom", prenomController), + _buildInput("Date de naissance (JJ/MM/AAAA)", dateController), + _buildInput("Email", emailController), + + const SizedBox(height: 30), + + // --- BOUTON JAUNE IDENTIQUE AUX AUTRES --- + GestureDetector( + onTap: () { + // TODO: action pour générer le certificat + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: const Color(0xFFFFD54F), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.orange.shade300), + ), + child: const Center( + child: Text( + "Valider et générer le certificat", + style: TextStyle( + color: Colors.black87, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildInput(String label, TextEditingController controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + const SizedBox(height: 6), + TextField( + controller: controller, + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade400), + ), + ), + ), + const SizedBox(height: 20), + ], + ); + } +} diff --git a/lib/ui/QCM/qcm_game_page.dart b/lib/ui/QCM/qcm_game_page.dart new file mode 100644 index 0000000..cad2060 --- /dev/null +++ b/lib/ui/QCM/qcm_game_page.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:factoscope/models/cours.dart'; + +import 'jeu_qcm_view_model.dart'; +import 'jeu_qcm_view.dart'; + +class QCMGamePage extends StatelessWidget { + final Cours cours; + + const QCMGamePage({super.key, required this.cours}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => JeuQCMViewModel()..chargerQCM(cours), + child: JeuQCMView(cours: cours), + ); + } +} diff --git a/lib/ui/QCM/qcm_officiel_view.dart b/lib/ui/QCM/qcm_officiel_view.dart new file mode 100644 index 0000000..79b491d --- /dev/null +++ b/lib/ui/QCM/qcm_officiel_view.dart @@ -0,0 +1,397 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'qcm_officiel_view_model.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/models/QCM/qcm.dart'; +import 'package:factoscope/ui/QCM/page_succes_qcm.dart'; + +class QCMOfficielView extends StatelessWidget { + final Cours cours; + + const QCMOfficielView({super.key, required this.cours}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) { + final vm = QCMOfficielViewModel(); + + vm.onSuccess = () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const PageSuccesQCM()), + ); + }; + + vm.onFailure = (score) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => PageEchecDetaillee( + score: score, + qcms: vm.controller!.qcmList, + userAnswers: vm.userAnswers, + ), + ), + ); + }; + + vm.chargerQCM(cours); + return vm; + }, + child: Consumer( + builder: (context, vm, child) { + if (vm.isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 2, + title: _buildTimer(vm), + centerTitle: true, + ), + body: _buildQuestion(context, vm), + ); + }, + ), + ); + } + + Widget _buildTimer(QCMOfficielViewModel vm) { + Color color; + + if (vm.duration > 180) { + color = Colors.green; + } else if (vm.duration > 60) { + color = Colors.orange; + } else { + color = Colors.red; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + "Temps restant : ${vm.timerText}", + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ); + } + + Widget _buildQuestion(BuildContext context, QCMOfficielViewModel vm) { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Question ${vm.currentIndex + 1} / ${vm.totalQuestions}", + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + + const SizedBox(height: 20), + + Text( + vm.questionText, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), + + const SizedBox(height: 20), + + Expanded( + child: ListView.builder( + itemCount: vm.options.length, + itemBuilder: (context, i) { + bool isSelected = vm.selectedIndex == i; + + return GestureDetector( + onTap: () => vm.selectAnswer(i), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.withValues(alpha: 0.15) + : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? Colors.blue : Colors.grey.shade300, + width: 2, + ), + ), + child: Text( + vm.options[i], + style: TextStyle( + fontSize: 16, + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? Colors.blue : Colors.black87, + ), + ), + ), + ); + }, + ), + ), + + const SizedBox(height: 20), + + // --- BOUTONS JAUNES --- + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: vm.currentIndex > 0 ? vm.previous : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14), + decoration: BoxDecoration( + color: vm.currentIndex > 0 + ? const Color(0xFFFFD54F) + : const Color(0xFFFFECB3), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.orange.shade300), + ), + child: const Row( + children: [ + Icon(Icons.arrow_back, color: Colors.black87), + SizedBox(width: 8), + Text( + "Précédent", + style: TextStyle( + color: Colors.black87, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + + GestureDetector( + onTap: vm.selectedIndex != null ? vm.next : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14), + decoration: BoxDecoration( + color: vm.selectedIndex != null + ? const Color(0xFFFFD54F) + : const Color(0xFFFFECB3), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.orange.shade300), + ), + child: Row( + children: [ + Text( + vm.currentIndex == vm.totalQuestions - 1 + ? "Terminer" + : "Suivant", + style: const TextStyle( + color: Colors.black87, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward, color: Colors.black87), + ], + ), + ), + ), + ], + ), + ], + ), + ); + } +} + +// +// PAGE D’ÉCHEC PROFESSIONNELLE +// + +class PageEchecDetaillee extends StatelessWidget { + final double score; + final List qcms; + final List userAnswers; + + const PageEchecDetaillee({ + super.key, + required this.score, + required this.qcms, + required this.userAnswers, + }); + + @override + Widget build(BuildContext context) { + final List wrongIndexes = []; + + for (int i = 0; i < qcms.length; i++) { + if (userAnswers[i] != qcms[i].soluce) { + wrongIndexes.add(i); + } + } + + return Scaffold( + appBar: AppBar( + title: const Text("Analyse des erreurs"), + centerTitle: true, + backgroundColor: Colors.white, + elevation: 2, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: wrongIndexes.isEmpty + ? _buildPerfectScore() + : _buildWrongAnswersList(wrongIndexes), + ), + ); + } + + Widget _buildPerfectScore() { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.emoji_events, color: Colors.amber, size: 80), + SizedBox(height: 20), + Text( + "Aucune erreur !", + style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold), + ), + SizedBox(height: 10), + Text( + "Vous avez répondu correctement à toutes les questions.", + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildWrongAnswersList(List wrongIndexes) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Vous avez ${wrongIndexes.length} erreur(s)", + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + + const SizedBox(height: 16), + + Expanded( + child: ListView.builder( + itemCount: wrongIndexes.length, + itemBuilder: (context, index) { + final qIndex = wrongIndexes[index]; + final q = qcms[qIndex]; + final user = userAnswers[qIndex]; + final correct = q.soluce; + + return Card( + elevation: 3, + margin: const EdgeInsets.symmetric(vertical: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.close, color: Colors.red, size: 26), + const SizedBox(width: 8), + Text( + "Question ${qIndex + 1}", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + + const SizedBox(height: 12), + + Text( + q.question, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + + const SizedBox(height: 12), + + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + const Icon(Icons.error, color: Colors.red), + const SizedBox(width: 8), + Expanded( + child: Text( + "Votre réponse : ${user != null ? q.getReponses()[user] : "Aucune"}", + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 10), + + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.green), + const SizedBox(width: 8), + Expanded( + child: Text( + "Bonne réponse : ${q.getReponses()[correct]}", + style: const TextStyle( + color: Colors.green, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/ui/QCM/qcm_officiel_view_model.dart b/lib/ui/QCM/qcm_officiel_view_model.dart new file mode 100644 index 0000000..7b593c2 --- /dev/null +++ b/lib/ui/QCM/qcm_officiel_view_model.dart @@ -0,0 +1,124 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:factoscope/models/QCM/qcm.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/repositories/QCM/qcm_controller.dart'; +import 'package:factoscope/repositories/QCM/qcm_repository.dart'; + +class QCMOfficielViewModel extends ChangeNotifier { + final QCMRepository _repo = QCMRepository(); + + QCMController? controller; + bool isLoading = true; + bool hasError = false; + + int duration = 300; + Timer? timer; + + List userAnswers = []; + int? selectedIndex; + + VoidCallback? onSuccess; + Function(double score)? onFailure; + + String get timerText { + int min = duration ~/ 60; + int sec = duration % 60; + return "$min:${sec.toString().padLeft(2, '0')}"; + } + + Future chargerQCM(Cours cours) async { + try { + isLoading = true; + notifyListeners(); + + List ids = await _repo.getAllIdByCoursId(cours.id!); + List qcms = []; + + for (int id in ids) { + QCM? q = await _repo.getById(id); + if (q != null) qcms.add(q); + } + + controller = QCMController(qcms); + controller!.start(); + + userAnswers = List.filled(qcms.length, null); + selectedIndex = null; + + _startTimer(); + + isLoading = false; + notifyListeners(); + } catch (e) { + hasError = true; + isLoading = false; + notifyListeners(); + } + } + + void _startTimer() { + timer = Timer.periodic(const Duration(seconds: 1), (t) { + duration--; + notifyListeners(); + + if (duration <= 0) { + t.cancel(); + _finishExam(); + } + }); + } + + String get questionText => controller!.currentQuestion.question; + List get options => controller!.currentQuestion.getReponses(); + int get currentIndex => controller!.currentIndex; + int get totalQuestions => controller!.qcmList.length; + + void selectAnswer(int index) { + selectedIndex = index; + userAnswers[currentIndex] = index; + controller!.selectAnswer(index); + notifyListeners(); + } + + void next() { + if (currentIndex < totalQuestions - 1) { + controller!.next(); + selectedIndex = userAnswers[currentIndex]; + if (selectedIndex != null) controller!.selectAnswer(selectedIndex!); + notifyListeners(); + } else { + _finishExam(); + } + } + + void previous() { + if (currentIndex > 0) { + controller!.previous(); + selectedIndex = userAnswers[currentIndex]; + if (selectedIndex != null) controller!.selectAnswer(selectedIndex!); + notifyListeners(); + } + } + + void _finishExam() { + int correct = 0; + + for (int i = 0; i < totalQuestions; i++) { + final q = controller!.qcmList[i]; + final userAnswer = userAnswers[i]; + + if (userAnswer != null && userAnswer == q.soluce) { + correct++; + } + } + + double score = correct / totalQuestions; + + if (score == 1.0) { + onSuccess?.call(); + } else { + onFailure?.call(score); + } + } +} diff --git a/lib/ui/about_view.dart b/lib/ui/about_view.dart new file mode 100644 index 0000000..b81f7e1 --- /dev/null +++ b/lib/ui/about_view.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; + +class AboutView extends StatelessWidget { + const AboutView({super.key}); + + @override + Widget build(BuildContext context) { + const brandBlue = Color.fromRGBO(41, 36, 96, 1); + const textColor = Color(0xFF666666); + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.arrow_back, color: brandBlue), + label: const Text( + "Retour à l'accueil", + style: TextStyle(color: brandBlue, fontWeight: FontWeight.bold), + ), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + ), + const SizedBox(height: 10), + const Text( + "À propos de Factoscope", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: brandBlue, + ), + ), + const Text( + "Version 1.0.0", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + _buildSectionTitle("Notre Mission", brandBlue), + const SizedBox(height: 12), + Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + "Factoscope est un outil pédagogique conçu pour vous accompagner dans votre formation. Notre objectif est de vous donner les clés pour décrypter l'information au quotidien.", + style: TextStyle( + fontSize: 16, + height: 1.5, + color: textColor, + ), + ), + ), + ), + const SizedBox(height: 24), + _buildSectionTitle("Fonctionnalités", brandBlue), + const SizedBox(height: 12), + _buildFeatureItem( + Icons.download_for_offline_rounded, + "Mode Hors-ligne", + "Téléchargez vos cours pour étudier partout, même sans connexion.", + brandBlue, + ), + _buildFeatureItem( + Icons.extension_rounded, + "Mini-jeux interactifs", + "Testez vos connaissances de manière ludique après chaque leçon.", + brandBlue, + ), + _buildFeatureItem( + Icons.workspace_premium_rounded, + "Validation des acquis", + "Obtenez un document officiel validant votre parcours de formation.", + brandBlue, + ), + const SizedBox(height: 40), + const Center( + child: Text( + "© 2026 Factoscope - Tous droits réservés", + style: TextStyle( + fontSize: 12, + color: Colors.grey, + fontStyle: FontStyle.italic, + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + Widget _buildSectionTitle(String title, Color color) { + return Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: color, + ), + ); + } + + Widget _buildFeatureItem( + IconData icon, String title, String description, Color iconColor) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: iconColor, size: 28), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 4), + Text( + description, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + ), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/all_cours_view.dart b/lib/ui/all_cours_view.dart new file mode 100644 index 0000000..5148e86 --- /dev/null +++ b/lib/ui/all_cours_view.dart @@ -0,0 +1,859 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide Page; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/repositories/cours_repository.dart'; +import 'package:factoscope/repositories/page_repository.dart'; +import 'package:factoscope/models/page.dart'; +import 'package:factoscope/ui/cours_selectionne.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +import '../config.dart'; +import 'api_service.dart'; +import '../models/QCM/qcm.dart'; +import '../models/Cloze/cloze_page.dart'; +import '../repositories/QCM/qcm_repository.dart'; +import '../repositories/Cloze/cloze_repository.dart'; + +class AllCoursView extends StatefulWidget { + const AllCoursView({super.key}); + + @override + State createState() => _AllCoursViewState(); +} + +class _AllCoursViewState extends State { + final CoursRepository _coursRepository = CoursRepository(); + final ApiService _apiService = ApiService(); + + List _coursLocaux = []; + List _coursDistants = []; + bool _isLoading = true; + bool _apiConnectee = false; + final Set _coursEnCoursDeTelechargement = {}; + List _titresTelecharges = []; + + @override + void initState() { + super.initState(); + _initialiserPage(); + } + + Future _initialiserPage() async { + setState(() => _isLoading = true); + + await _verifierConnexionApi(); + await _chargerCoursLocaux(); + + if (_apiConnectee) { + await _chargerCoursDistants(); + } + + if (mounted) setState(() => _isLoading = false); + } + + Future _verifierConnexionApi() async { + final connectee = await _apiService.testConnection(); + _apiConnectee = connectee; + if (mounted) setState(() {}); + } + + Future _chargerCoursLocaux() async { + try { + final cours = await _coursRepository.getAll(); + if (mounted) { + setState(() { + _coursLocaux = cours; + _titresTelecharges = cours.map((c) => c.titre).toList(); + }); + } + } catch (e) { + if (kDebugMode) print('Erreur chargement cours locaux: $e'); + } + } + + Future _chargerCoursDistants() async { + try { + final distants = await _apiService.getCoursDisponibles(); + if (mounted) setState(() => _coursDistants = distants); + } catch (e) { + if (kDebugMode) print('Erreur chargement cours distants: $e'); + } + } + + Future _rafraichir() async { + setState(() => _isLoading = true); + await _verifierConnexionApi(); + await _chargerCoursLocaux(); + if (_apiConnectee) await _chargerCoursDistants(); + if (mounted) setState(() => _isLoading = false); + } + + Future _telechargerCours(CoursDistant cours) async { + setState(() => _coursEnCoursDeTelechargement.add(cours.id)); + + try { + final coursComplet = await _apiService.getCoursComplet(cours.id); + await _sauvegarderCoursLocalement(coursComplet); + await _chargerCoursLocaux(); + + if (mounted) { + setState(() => _titresTelecharges.add(cours.titre)); + _afficherPopupSucces(cours.titre); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors du téléchargement: $e'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } finally { + if (mounted) { + setState(() => _coursEnCoursDeTelechargement.remove(cours.id)); + } + } + } + + Future _toutTelecharger() async { + final coursATelechargement = _coursDistants + .where((c) => !_titresTelecharges.contains(c.titre)) + .toList(); + + if (coursATelechargement.isEmpty) return; + + setState(() { + for (final c in coursATelechargement) { + _coursEnCoursDeTelechargement.add(c.id); + } + }); + + int succes = 0; + final List erreurs = []; + + for (final cours in coursATelechargement) { + try { + final coursComplet = await _apiService.getCoursComplet(cours.id); + await _sauvegarderCoursLocalement(coursComplet); + if (mounted) { + setState(() => _titresTelecharges.add(cours.titre)); + } + succes++; + } catch (e) { + erreurs.add(cours.titre); + } finally { + if (mounted) { + setState(() => _coursEnCoursDeTelechargement.remove(cours.id)); + } + } + } + + await _chargerCoursLocaux(); + + if (!mounted) return; + + if (erreurs.isEmpty) { + _afficherPopupToutTelecharge(succes); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '$succes cours téléchargé(s). Échec : ${erreurs.join(', ')}', + ), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 4), + ), + ); + } + } + + Future _sauvegarderCoursLocalement(CoursComplet coursComplet) async { + if (kDebugMode) { + print('📚 Début sauvegarde cours: "${coursComplet.cours.titre}" (module ${coursComplet.cours.idModule})'); + } + final pageRepository = PageRepository(); + + final coursLocal = Cours( + idModule: coursComplet.cours.idModule, + titre: coursComplet.cours.titre, + contenu: coursComplet.cours.contenu, + description: coursComplet.cours.description, + ); + + final coursIdLocal = await _coursRepository.create(coursLocal); + if (kDebugMode) { + print('✅ Cours créé en BDD locale avec id: $coursIdLocal'); + } + + final dossierCours = await _creerDossierCours( + idModule: coursComplet.cours.idModule, + idCours: coursIdLocal, + ); + + // ── Pages ────────────────────────────────────────────────────────────── + final List tousLesMedias = []; + for (final page in coursComplet.pages) { + if (kDebugMode) { + print(' 📄 Page "${page.description}" — medias bruts: "${page.medias}"'); + } + if (page.medias.isNotEmpty) { + final noms = page.medias.split('@') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + if (kDebugMode) print(' └─ Médias extraits: $noms'); + tousLesMedias.addAll(noms); + } + } + + if (kDebugMode) { + print('🎯 Total médias à télécharger: ${tousLesMedias.length} → $tousLesMedias'); + } + + await _telechargerTousLesMediasDuCours( + idModule: coursComplet.cours.idModule, + idCours: coursIdLocal, + nomsMedias: tousLesMedias, + dossierCours: dossierCours, + ); + + for (final pageDistante in coursComplet.pages) { + final List mediasList = []; + + if (pageDistante.medias.isNotEmpty) { + final noms = pageDistante.medias.split('@') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + + for (int i = 0; i < noms.length; i++) { + final nomFichier = noms[i]; + String typeMedia = 'text'; + final ext = nomFichier.split('.').last.toLowerCase(); + if (['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) { + typeMedia = 'image'; + } else if (['mp4', 'webm', 'avi'].contains(ext)) { + typeMedia = 'video'; + } else if (['mp3', 'wav', 'ogg'].contains(ext)) { + typeMedia = 'audio'; + } + + final cheminLocal = path.join(dossierCours.path, nomFichier); + if (kDebugMode) { + print(' 💾 MediaItem: $nomFichier → type=$typeMedia → $cheminLocal'); + } + + mediasList.add(MediaItem( + ordre: i + 1, + url: cheminLocal, + type: typeMedia, + caption: '', + )); + } + } + + final pageLocale = Page( + idCours: coursIdLocal, + description: pageDistante.description, + contenu: pageDistante.contenu, + medias: mediasList, + ); + + await pageRepository.create(pageLocale); + if (kDebugMode) { + print(' ✅ Page "${pageDistante.description}" sauvegardée'); + } + } + + // ── QCM ─────────────────────────────────────────────────────────────── + await _sauvegarderQcm( + coursIdDistant: coursComplet.cours.id, + coursIdLocal: coursIdLocal, + ); + + // ── Cloze (texte à trou) ────────────────────────────────────────────── + await _sauvegarderCloze( + coursIdDistant: coursComplet.cours.id, + coursIdLocal: coursIdLocal, + ); + + if (kDebugMode) { + print('🏁 Sauvegarde terminée pour "${coursComplet.cours.titre}"'); + } + } + + /// Télécharge les QCM distants et les insère en BDD locale + Future _sauvegarderQcm({ + required int coursIdDistant, + required int coursIdLocal, + }) async { + try { + final qcmDistants = await _apiService.getQcmDuCours(coursIdDistant); + + if (kDebugMode) { + print('🎯 QCM à sauvegarder: ${qcmDistants.length}'); + } + + if (qcmDistants.isEmpty) return; + + final qcmRepository = QCMRepository(); + + for (final q in qcmDistants) { + final qcmLocal = QCM( + question: q.question, + rep1: q.rep1, + rep2: q.rep2, + rep3: q.rep3, + rep4: q.rep4, + soluce: q.soluce, + idCours: coursIdLocal, + ); + await qcmRepository.insert(qcmLocal); + if (kDebugMode) { + print(' ✅ QCM inséré: "${q.question}"'); + } + } + } catch (e) { + // On ne bloque pas le téléchargement du cours si les QCM échouent + if (kDebugMode) print('⚠️ Erreur sauvegarde QCM: $e'); + } + } + + /// Télécharge les Cloze distants et les insère en BDD locale + Future _sauvegarderCloze({ + required int coursIdDistant, + required int coursIdLocal, + }) async { + try { + final clozeDistants = await _apiService.getClozesDuCours(coursIdDistant); + + if (kDebugMode) { + print('🧩 Cloze à sauvegarder: ${clozeDistants.length}'); + } + + if (clozeDistants.isEmpty) return; + + final clozeRepository = ClozeRepository(); + + for (final c in clozeDistants) { + final clozeLocal = ClozeQuestion( + phrase: c.texte, + rep1: c.reponse1, + rep2: c.reponse2, + rep3: c.reponse3, + rep4: c.reponse4, + soluce: c.numeroReponseCorrecte, + idCours: coursIdLocal, + ); + await clozeRepository.insert(clozeLocal); + if (kDebugMode) { + print(' ✅ Cloze inséré: "${c.texte.substring(0, c.texte.length.clamp(0, 40))}..."'); + } + } + } catch (e) { + // On ne bloque pas le téléchargement du cours si les Cloze échouent + if (kDebugMode) print('⚠️ Erreur sauvegarde Cloze: $e'); + } + } + + Future _creerDossierCours({ + required int idModule, + required int idCours, + }) async { + final appDir = await getApplicationDocumentsDirectory(); + final dossier = Directory( + path.join(appDir.path, 'AppData', 'Module$idModule', 'Cours$idCours'), + ); + if (!await dossier.exists()) { + await dossier.create(recursive: true); + } + return dossier; + } + + Future _telechargerTousLesMediasDuCours({ + required int idModule, + required int idCours, + required List nomsMedias, + required Directory dossierCours, + }) async { + final String baseUrl = + '${AppConfig.urlMedias}/AppData/Module$idModule/Cours$idCours'; + if (kDebugMode) print('🌐 Base URL médias: $baseUrl'); + + if (nomsMedias.isEmpty) { + if (kDebugMode) print('⚠️ Aucun média à télécharger'); + return; + } + + final futures = nomsMedias.map((nomFichier) async { + final urlComplete = '$baseUrl/$nomFichier'; + final fichier = File(path.join(dossierCours.path, nomFichier)); + + if (await fichier.exists()) { + if (kDebugMode) print('⏭️ $nomFichier déjà présent, skip'); + return; + } + + if (kDebugMode) print('⬇️ Téléchargement: $urlComplete'); + try { + final response = await http.get(Uri.parse(urlComplete)); + if (kDebugMode) { + print(' └─ Status: ${response.statusCode} — ${response.bodyBytes.length} bytes'); + } + if (response.statusCode == 200) { + await fichier.writeAsBytes(response.bodyBytes); + if (kDebugMode) print(' └─ ✅ Sauvegardé: ${fichier.path}'); + } else { + if (kDebugMode) print(' └─ ❌ Introuvable (${response.statusCode}): $urlComplete'); + } + } catch (e) { + if (kDebugMode) print(' └─ ❌ Erreur pour $nomFichier: $e'); + } + }); + + await Future.wait(futures); + if (kDebugMode) print('✅ Tous les médias téléchargés'); + } + + void _afficherPopupSucces(String titreCours) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: const Icon(Icons.check, color: Colors.white, size: 50), + ), + const SizedBox(height: 20), + const Text( + 'Téléchargement réussi !', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + '"$titreCours"', + style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + const Text( + 'Le cours et ses jeux ont été ajoutés à votre bibliothèque', + style: TextStyle(fontSize: 14, color: Colors.grey), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('OK', style: TextStyle(fontSize: 16)), + ), + ], + ), + ), + ), + ); + } + + void _afficherPopupToutTelecharge(int nb) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: const Icon(Icons.cloud_done, color: Colors.white, size: 46), + ), + const SizedBox(height: 20), + const Text( + 'Tout est téléchargé !', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + '$nb cours ajouté${nb > 1 ? 's' : ''} à votre bibliothèque', + style: const TextStyle(fontSize: 15, color: Colors.grey), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), + ), + child: const Text('OK', style: TextStyle(fontSize: 16)), + ), + ], + ), + ), + ), + ); + } + + bool _estTelecharge(String titre) => _titresTelecharges.contains(titre); + + Cours? _coursLocalPourTitre(String titre) { + try { + return _coursLocaux.firstWhere((c) => c.titre == titre); + } catch (_) { + return null; + } + } + + List<_CoursItem> get _listeUnifiee { + final List<_CoursItem> telecharges = []; + final List<_CoursItem> nonTelecharges = []; + + for (final distant in _coursDistants) { + if (_estTelecharge(distant.titre)) { + final local = _coursLocalPourTitre(distant.titre); + if (local != null) { + telecharges.add(_CoursItem.local(local)); + } + } else { + nonTelecharges.add(_CoursItem.distant(distant)); + } + } + + for (final local in _coursLocaux) { + final dejaDans = telecharges.any((item) => item.titre == local.titre); + if (!dejaDans) { + telecharges.add(_CoursItem.local(local)); + } + } + + return [...telecharges, ...nonTelecharges]; + } + + @override + Widget build(BuildContext context) { + final liste = _listeUnifiee; + final nbTelecharges = liste.where((i) => i.estTelecharge).length; + final nbDistants = liste.where((i) => !i.estTelecharge).length; + + return Scaffold( + appBar: AppBar( + title: const Text('Mes Cours'), + centerTitle: true, + backgroundColor: Colors.white, + titleTextStyle: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + actions: [ + IconButton( + icon: Icon( + Icons.cloud, + color: _apiConnectee ? Colors.green : Colors.grey, + ), + onPressed: _rafraichir, + tooltip: _apiConnectee + ? 'API connectée — Rafraîchir' + : 'API non disponible — Réessayer', + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: _rafraichir, + child: liste.isEmpty + ? _buildEtatVide() + : CustomScrollView( + slivers: [ + if (nbTelecharges > 0) + _buildSectionHeader( + 'Téléchargés', + '$nbTelecharges cours', + Colors.black87, + ), + SliverPadding( + padding: const EdgeInsets.only( + left: 16, right: 16, bottom: 24), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final item = liste[index]; + final estPremierNonTelecharge = + !item.estTelecharge && + (index == 0 || + liste[index - 1].estTelecharge); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (estPremierNonTelecharge) ...[ + if (nbTelecharges > 0) + const SizedBox(height: 8), + _buildSectionLabel( + 'Disponibles en ligne', + '$nbDistants cours', + ), + ], + _buildCoursCard(item), + ], + ); + }, + childCount: liste.length, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSectionHeader(String titre, String sousTitre, Color couleur) { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 4), + child: Row( + children: [ + Text( + titre, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: couleur, + ), + ), + const SizedBox(width: 8), + Text( + sousTitre, + style: const TextStyle(fontSize: 13, color: Colors.grey), + ), + ], + ), + ), + ); + } + + Widget _buildSectionLabel(String titre, String sousTitre) { + final bool toutEnCours = _coursEnCoursDeTelechargement.isNotEmpty; + + return Padding( + padding: const EdgeInsets.fromLTRB(4, 16, 4, 4), + child: Row( + children: [ + Text( + titre, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + const SizedBox(width: 8), + Text( + sousTitre, + style: const TextStyle(fontSize: 13, color: Colors.grey), + ), + const Spacer(), + toutEnCours + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : TextButton.icon( + onPressed: _toutTelecharger, + icon: const Icon(Icons.download, size: 16), + label: const Text('Tout télécharger', + style: TextStyle(fontSize: 13)), + style: TextButton.styleFrom( + foregroundColor: Colors.grey[700], + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ], + ), + ); + } + + Widget _buildCoursCard(_CoursItem item) { + final enTelechargement = item.distantId != null && + _coursEnCoursDeTelechargement.contains(item.distantId); + + const couleurAccent = Color.fromRGBO(252, 179, 48, 1); + + return Card( + margin: const EdgeInsets.symmetric(vertical: 6), + elevation: item.estTelecharge ? 2 : 1, + color: item.estTelecharge ? Colors.white : Colors.grey[100], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () { + if (item.estTelecharge && item.coursLocal != null) { + CoursSelectionne.instance.setCours(item.coursLocal!); + GoRouter.of(context).go('/cours/${item.coursLocal!.id}'); + } else if (item.coursDistant != null && !enTelechargement) { + _telechargerCours(item.coursDistant!); + } + }, + child: Padding( + padding: const EdgeInsets.all(14), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: item.estTelecharge + ? couleurAccent + : Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.school, + color: item.estTelecharge ? Colors.white : Colors.grey[500], + size: 28, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.titre, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: item.estTelecharge + ? Colors.black87 + : Colors.grey[600], + ), + ), + if (item.description.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + item.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: item.estTelecharge + ? Colors.black54 + : Colors.grey[400], + ), + ), + ], + ], + ), + ), + const SizedBox(width: 8), + if (item.estTelecharge) + const Icon(Icons.arrow_forward_ios, + size: 16, color: Colors.grey) + else if (enTelechargement) + const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + Icon(Icons.download_outlined, + color: Colors.grey[500], size: 26), + ], + ), + ), + ), + ); + } + + Widget _buildEtatVide() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.school_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + _apiConnectee + ? 'Aucun cours disponible' + : 'Aucun cours téléchargé\net API non disponible', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 16), + TextButton.icon( + onPressed: _rafraichir, + icon: const Icon(Icons.refresh), + label: const Text('Réessayer'), + ), + ], + ), + ); + } +} + +class _CoursItem { + final String titre; + final String description; + final bool estTelecharge; + final Cours? coursLocal; + final CoursDistant? coursDistant; + + _CoursItem.local(Cours c) + : titre = c.titre, + description = c.contenu, + estTelecharge = true, + coursLocal = c, + coursDistant = null; + + _CoursItem.distant(CoursDistant c) + : titre = c.titre, + description = c.description, + estTelecharge = false, + coursLocal = null, + coursDistant = c; + + int? get distantId => coursDistant?.id; +} \ No newline at end of file diff --git a/lib/ui/api_service.dart b/lib/ui/api_service.dart new file mode 100644 index 0000000..eb0965c --- /dev/null +++ b/lib/ui/api_service.dart @@ -0,0 +1,288 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:flutter/foundation.dart'; +import '../config.dart'; + +class ApiService { + static final ApiService _instance = ApiService._internal(); + factory ApiService() => _instance; + ApiService._internal(); + + Future> getCoursDisponibles() async { + try { + final response = await http.get( + Uri.parse('${AppConfig.effectiveApiUrl}/api/cours'), + headers: {'Content-Type': 'application/json'}, + ).timeout(Duration(seconds: AppConfig.apiTimeout)); + + if (response.statusCode == 200) { + final List data = json.decode(response.body); + final coursList = []; + for (var jsonItem in data) { + try { + final cours = CoursDistant.fromJson(jsonItem); + coursList.add(cours); + } catch (e) { + if (kDebugMode) { + print('Erreur parsing cours: $e'); + print(' JSON: $jsonItem'); + } + } + } + return coursList; + } else { + throw Exception('Erreur HTTP ${response.statusCode}: ${response.body}'); + } + } catch (e) { + throw Exception('Erreur de connexion à l\'API: $e'); + } + } + + Future getCoursComplet(int coursId) async { + try { + final response = await http.get( + Uri.parse('${AppConfig.effectiveApiUrl}/api/cours/$coursId'), + headers: {'Content-Type': 'application/json'}, + ).timeout(Duration(seconds: AppConfig.apiTimeout)); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + + final pagesResponse = await http.get( + Uri.parse('${AppConfig.effectiveApiUrl}/api/pages'), + headers: {'Content-Type': 'application/json'}, + ).timeout(Duration(seconds: AppConfig.apiTimeout)); + + List pages = []; + if (pagesResponse.statusCode == 200) { + final List pagesData = json.decode(pagesResponse.body); + pages = pagesData + .where((p) => p['id_cours'] == coursId) + .map((json) => PageDistante.fromJson(json)) + .toList(); + } + + return CoursComplet.fromJson(data, pages); + } else { + throw Exception('Erreur HTTP ${response.statusCode}'); + } + } catch (e) { + throw Exception('Erreur de connexion à l\'API: $e'); + } + } + + /// Récupère les questions QCM associées à un cours depuis l'API + Future> getQcmDuCours(int coursId) async { + try { + final response = await http.get( + Uri.parse('${AppConfig.effectiveApiUrl}/api/qcm/$coursId'), + headers: {'Content-Type': 'application/json'}, + ).timeout(Duration(seconds: AppConfig.apiTimeout)); + + if (response.statusCode == 200) { + final List data = json.decode(response.body); + final list = []; + for (var jsonItem in data) { + try { + list.add(QcmDistant.fromJson(jsonItem)); + } catch (e) { + if (kDebugMode) { + print('Erreur parsing QCM: $e'); + print(' JSON: $jsonItem'); + } + } + } + return list; + } else { + throw Exception('Erreur HTTP ${response.statusCode}: ${response.body}'); + } + } catch (e) { + throw Exception('Erreur de connexion à l\'API (QCM): $e'); + } + } + + /// Récupère les questions Cloze (texte à trou) associées à un cours depuis l'API + Future> getClozesDuCours(int coursId) async { + try { + final response = await http.get( + Uri.parse('${AppConfig.effectiveApiUrl}/api/text-a-true/$coursId'), + headers: {'Content-Type': 'application/json'}, + ).timeout(Duration(seconds: AppConfig.apiTimeout)); + + if (response.statusCode == 200) { + final List data = json.decode(response.body); + final list = []; + for (var jsonItem in data) { + try { + list.add(ClozeDistant.fromJson(jsonItem)); + } catch (e) { + if (kDebugMode) { + print('Erreur parsing Cloze: $e'); + print(' JSON: $jsonItem'); + } + } + } + return list; + } else { + throw Exception('Erreur HTTP ${response.statusCode}: ${response.body}'); + } + } catch (e) { + throw Exception('Erreur de connexion à l\'API (Cloze): $e'); + } + } + + Future testConnection() async { + try { + final response = await http.get( + Uri.parse('${AppConfig.effectiveApiUrl}/'), + headers: {'Content-Type': 'application/json'}, + ).timeout(const Duration(seconds: 5)); + + return response.statusCode == 200; + } catch (e) { + return false; + } + } +} + +class CoursDistant { + final int id; + final String titre; + final String description; + final String contenu; + final int idModule; + + CoursDistant({ + required this.id, + required this.titre, + required this.description, + required this.contenu, + required this.idModule, + }); + + factory CoursDistant.fromJson(Map json) { + return CoursDistant( + id: json['id'] as int, + titre: json['titre']?.toString() ?? '', + description: json['description']?.toString() ?? '', + contenu: json['contenu']?.toString() ?? '', + idModule: (json['id_module'] as int?) ?? 0, + ); + } +} + +class PageDistante { + final int id; + final String description; + final String contenu; + final String medias; + final int estVue; + final int idCours; + + PageDistante({ + required this.id, + required this.description, + required this.contenu, + required this.medias, + required this.estVue, + required this.idCours, + }); + + factory PageDistante.fromJson(Map json) { + return PageDistante( + id: json['id'] as int, + description: json['description']?.toString() ?? '', + contenu: json['content'] as String? ?? '', + medias: json['medias']?.toString() ?? '', + estVue: (json['est_vue'] as int?) ?? 0, + idCours: json['id_cours'] as int, + ); + } +} + +class CoursComplet { + final CoursDistant cours; + final List pages; + + CoursComplet({required this.cours, required this.pages}); + + factory CoursComplet.fromJson(Map json, List pages) { + return CoursComplet( + cours: CoursDistant.fromJson(json), + pages: pages, + ); + } +} + +class QcmDistant { + final int id; + final String question; + final String rep1; + final String rep2; + final String rep3; + final String rep4; + final int soluce; + final int idCours; + + QcmDistant({ + required this.id, + required this.question, + required this.rep1, + required this.rep2, + required this.rep3, + required this.rep4, + required this.soluce, + required this.idCours, + }); + + factory QcmDistant.fromJson(Map json) { + return QcmDistant( + id: json['id'] as int, + question: json['question']?.toString() ?? '', + rep1: json['rep1']?.toString() ?? '', + rep2: json['rep2']?.toString() ?? '', + rep3: json['rep3']?.toString() ?? '', + rep4: json['rep4']?.toString() ?? '', + soluce: (json['soluce'] as int?) ?? 1, + idCours: json['id_cours'] as int, + ); + } +} + +class ClozeDistant { + final int id; + final String texte; + final String reponse1; + final String reponse2; + final String reponse3; + final String reponse4; + final int numeroReponseCorrecte; + final String? explication; + final int idCours; + + ClozeDistant({ + required this.id, + required this.texte, + required this.reponse1, + required this.reponse2, + required this.reponse3, + required this.reponse4, + required this.numeroReponseCorrecte, + this.explication, + required this.idCours, + }); + + factory ClozeDistant.fromJson(Map json) { + return ClozeDistant( + id: json['id'] as int, + texte: json['texte']?.toString() ?? '', + reponse1: json['reponse1']?.toString() ?? '', + reponse2: json['reponse2']?.toString() ?? '', + reponse3: json['reponse3']?.toString() ?? '', + reponse4: json['reponse4']?.toString() ?? '', + numeroReponseCorrecte: (json['numero_reponse_correcte'] as int?) ?? 1, + explication: json['explication']?.toString(), + idCours: json['id_cours'] as int, + ); + } +} \ No newline at end of file diff --git a/lib/ui/app.dart b/lib/ui/app.dart new file mode 100644 index 0000000..72f539b --- /dev/null +++ b/lib/ui/app.dart @@ -0,0 +1,163 @@ +import 'package:factoscope/ui/validation_view.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:factoscope/ui/LaunchScreen/launch_screen_view.dart'; +import 'package:factoscope/ui/list_module_view.dart'; +import 'package:factoscope/ui/Cours/cours_view.dart'; +import 'package:factoscope/ui/all_cours_view.dart'; +import 'list_cours_view.dart'; +import 'package:factoscope/ui/about_view.dart'; + +final _rootNavigatorKey = GlobalKey(); +final _shellNavigatorKey = GlobalKey(); + +final router = GoRouter( + navigatorKey: _rootNavigatorKey, + routes: [ + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (context, state, child) => App(child: child), + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const ListModulesView(), + ), + GoRoute( + path: '/cours', + builder: (context, state) => const AllCoursView(), + ), + GoRoute( + path: '/cours/:coursId', + builder: (context, state) { + final coursId = int.parse(state.pathParameters['coursId']!); + return CoursView(coursId: coursId); + }, + ), + GoRoute( + path: '/list_cours', + builder: (context, state) => ListCoursView(), + ), + GoRoute( + path: '/validation', + builder: (context, state) => const ValidationView(), + ), + GoRoute( + path: '/about', + builder: (context, state) => const AboutView(), + ), + ], + ), + ], +); + +class App extends StatefulWidget { + const App({super.key, this.child}); + + final Widget? child; + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + bool showLaunchScreen = true; + + @override + void initState() { + super.initState(); + Future.delayed(const Duration(milliseconds: 4000), () { + if (mounted) { + setState(() => showLaunchScreen = false); + } + }); + } + + /// Détermine l'index actif de la navbar en fonction de la route courante. + int _indexFromLocation(String location) { + if (location.startsWith('/cours')) return 1; + if (location.startsWith('/validation')) return 2; + return 0; + } + + void _changeTab(BuildContext context, int index) { + switch (index) { + case 0: + context.go('/'); + break; + case 1: + context.go('/cours'); + break; + case 2: + context.go('/validation'); + break; + default: + context.go('/'); + } + } + + @override + Widget build(BuildContext context) { + // On lit la route courante depuis GoRouter pour garder la navbar en sync + // même lors des navigations internes (ex: /cours/42). + final location = GoRouterState.of(context).matchedLocation; + final currentIndex = _indexFromLocation(location); + + return Stack( + children: [ + Scaffold( + appBar: AppBar( + backgroundColor: const Color.fromRGBO(252, 179, 48, 1), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'lib/assets/logo-factoscope_seul_2.png', + height: 75, + width: 350, + fit: BoxFit.contain, + ), + const SizedBox(width: 10), + ], + ), + centerTitle: true, + ), + body: widget.child!, + bottomNavigationBar: Container( + decoration: const BoxDecoration( + color: Color.fromRGBO(41, 36, 96, 1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: BottomNavigationBar( + onTap: (index) => _changeTab(context, index), + backgroundColor: Colors.transparent, + currentIndex: currentIndex, + unselectedItemColor: Colors.white, + selectedItemColor: const Color.fromRGBO(252, 179, 48, 1), + items: const [ + BottomNavigationBarItem( + icon: Padding( + padding: EdgeInsets.only(top: 5), + child: Icon(Icons.home), + ), + label: 'Accueil', + ), + BottomNavigationBarItem( + icon: Icon(Icons.book), + label: 'Formation', + ), + BottomNavigationBarItem( + icon: Icon(Icons.verified), + label: 'Validation', + ), + ], + ), + ), + ), + if (showLaunchScreen) const LaunchScreenView(), + ], + ); + } +} \ No newline at end of file diff --git a/lib/ui/cours_selectionne.dart b/lib/ui/cours_selectionne.dart new file mode 100644 index 0000000..5a3e0e4 --- /dev/null +++ b/lib/ui/cours_selectionne.dart @@ -0,0 +1,21 @@ +import 'package:flutter/foundation.dart'; +import 'package:factoscope/models/cours.dart'; + +class CoursSelectionne with ChangeNotifier { + static final CoursSelectionne instance = CoursSelectionne._internal(); + factory CoursSelectionne() => instance; + CoursSelectionne._internal(); + + Cours cours = Cours( + idModule: 0, + titre: "UTILISE POUR INIT LE SINGLETON COURSSELECTIONNE", + contenu: "UTILISE POUR INIT LE SINGLETON COURSSELECTIONNE", + description: "UTILISE POUR INIT LE SINGLETON COURSSELECTIONNE", + isDownloaded: 0, + ); + + void setCours(Cours nouveauCours) { + cours = nouveauCours; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/ui/list_cours_view.dart b/lib/ui/list_cours_view.dart new file mode 100644 index 0000000..cce4b18 --- /dev/null +++ b/lib/ui/list_cours_view.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:factoscope/models/list_cours_view_model.dart'; +import 'package:factoscope/models/page.dart'; +import 'package:factoscope/ui/Contenu/WidgetContenu/contenu_image_widget.dart'; +import 'package:factoscope/ui/cours_selectionne.dart'; + +import '../models/cours.dart'; +import '../models/module.dart'; +import 'module_selectionne.dart'; + +import 'package:go_router/go_router.dart'; + +class ListCoursView extends StatefulWidget { + final ListCoursViewModel listCours = ListCoursViewModel(); + final ModuleSelectionne moduleSelectionne = ModuleSelectionne(); + + ListCoursView({super.key}); + + @override + State createState() => ListCoursViewState(); +} + +class ListCoursViewState extends State { + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: widget.moduleSelectionne, + builder: (context, child) { + int size = 0; + Module module = widget.moduleSelectionne.moduleSelectionne; + widget.listCours.getCours(module.id); + size = widget.moduleSelectionne.coursDuModule.length; + + return ListenableBuilder( + listenable: widget.listCours, + builder: (context, child) { + return Column( + children: [ + FutureBuilder( + future: + ListCoursViewModel().getProgressionModule(module), + builder: (context, snapshot) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 20.0), + child: moduleHeader(module, snapshot.data)); + }), + Expanded( + child: ListView.builder( + itemCount: size + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Align( + alignment: Alignment.centerLeft, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 5.0), + child: const Text( + "Description et Objectif du Module", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, + ), + ), + Container( + margin: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 8.0), + child: Text( + module.description, + style: const TextStyle( + fontSize: 18, + color: Colors.black, + ), + textAlign: TextAlign.left, + )), + ], + ), + ); + } else { + final item = + widget.moduleSelectionne.coursDuModule[index - 1]; + return listItem(item, context); + } + }, + ), + ), + ], + ); + }); + }); + } +} + +// Widget correspondant au titre du module +SizedBox moduleHeader(Module module, double? progress) { + MediaItem media = MediaItem( + ordre: 1, + url: module.urlImg, + type: "image", + ); + + return SizedBox( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), + margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: const Color.fromARGB(255, 236, 187, 139), + ), + child: Row( + children: [ + ContenuImageWidget( + media: media, + width: 80, + height: 80, + ), + const Spacer(), + Column( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5.0, vertical: 5.0), + margin: const EdgeInsets.symmetric( + horizontal: 5.0, vertical: 5.0), + child: Text( + module.titre, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + overflow: TextOverflow.clip, + ), + ), + SizedBox( + width: 200, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progress, + minHeight: 6, + color: const Color.fromARGB(255, 90, 230, 220), + backgroundColor: + const Color.fromARGB(255, 175, 240, 235), + )), + ) + ], + ), + const Spacer() + ], + ), + )); +} + +// Widget permettant d'afficher et de sélectionner un cours +SizedBox listItem(Cours cours, BuildContext context) { + return SizedBox( + child: InkWell( + onTap: () { + CoursSelectionne.instance.setCours(cours); + GoRouter.of(context).go('/cours/${cours.id}'); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), + margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + child: FutureBuilder( + future: ListCoursViewModel().getProgressionCours(cours), + builder: (context, snapshot) { + final double? progression = snapshot.data; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 15.0, vertical: 20.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: const Color.fromARGB(255, 235, 235, 235), + ), + child: Row( + children: [ + // Icône du cours + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + color: const Color.fromARGB(255, 236, 187, 139), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.school, + color: Colors.white, size: 26), + ), + const SizedBox(width: 14), + // Titre + barre de progression + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + cours.titre, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + if (cours.contenu.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + cours.contenu, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, color: Colors.black54), + ), + ], + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progression ?? 0.0, + minHeight: 5, + color: const Color.fromARGB(255, 90, 230, 220), + backgroundColor: + const Color.fromARGB(255, 175, 240, 235), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + const Icon(Icons.arrow_forward_ios, + size: 14, color: Colors.grey), + ], + ), + ); + }, + ), + ), + ), + ); +} \ No newline at end of file diff --git a/lib/ui/list_module_view.dart b/lib/ui/list_module_view.dart new file mode 100644 index 0000000..10448d9 --- /dev/null +++ b/lib/ui/list_module_view.dart @@ -0,0 +1,309 @@ +import 'package:flutter/material.dart'; +import 'package:factoscope/models/module.dart'; +import 'package:factoscope/ui/module_selectionne.dart'; +import 'package:factoscope/models/list_module_view_model.dart'; +import 'package:go_router/go_router.dart'; + +// Widget de la page d'accueil/Liste des modules +class ListModulesView extends StatefulWidget { + const ListModulesView({super.key}); + + @override + State createState() => _ListModulesViewState(); +} + +// State du widget d'affichage de la liste des modules (page d'accueil) +class _ListModulesViewState extends State { + ListModuleViewModel listModuleViewModel = ListModuleViewModel(); + + @override + void initState() { + super.initState(); + listModuleViewModel + .recupererModule(); // Charge les modules dès l'initialisation + } + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: listModuleViewModel, + builder: (context, child) { + return SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Accueil", + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + IconButton( + icon: const Icon(Icons.info_outline, size: 30), + onPressed: () => context.push('/about'), + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + child: Text( + "Factoscope est un outil pédagogique conçu pour vous accompagner dans votre formation. Notre objectif est de vous donner les clés pour décrypter l'information au quotidien.", + style: TextStyle( + fontSize: 16, + color: Colors.black54, + ), + ), + ), + + const Divider(height: 1), + + Align( + alignment: Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 5.0), + child: const Text( + "Tableau de bord", + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + ), + ), + headerAvancement(), // Appel à headerAvancement + + const Divider(height: 1), + + Align( + alignment: Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 5.0), + child: const Text( + "Cours récemment vus :", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.left, + ), + ), + ), + + const SizedBox(height: 30), + const Divider(height: 1), + const SizedBox(height: 20), + + const Text( + "Avec le soutien de", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black54, + ), + ), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _partnerLogo('lib/assets/cfi.jpg'), + _partnerLogo('lib/assets/epjt.png'), + _partnerLogo('lib/assets/nothing2hide.jpg'), + ], + ), + ), + const SizedBox(height: 40), + ], + ), + ); + }, + ); + } + + Widget _partnerLogo(String assetPath) { + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Image.asset( + assetPath, + height: 60, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 60, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.business, color: Colors.grey), + ); + }, + ), + ), + ); + } +} + +// Widget représentant l'header d'un module dans la liste des modules +SizedBox listModuleItem(Module item, BuildContext context) { + return SizedBox( + child: InkWell( + child: FutureBuilder( + future: ListModuleViewModel().getProgressionModule(item), + builder: (context, snapshot) { + return moduleHeader(item, snapshot.data); + }, + ), + onTap: () { + ModuleSelectionne.instance.moduleSelectionne = item; + GoRouter.of(context).push('/list_cours'); + }, + ), + ); +} + +SizedBox moduleHeader(Module module, double? progress) { + return SizedBox( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), + margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: const Color.fromARGB(255, 236, 187, 139), + ), + child: Row( + children: [ + // Image du module + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + module.urlImg, + width: 80, + height: 80, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 80, + height: 80, + color: Colors.grey[300], + child: const Icon(Icons.image_not_supported, size: 40), + ); + }, + ), + ), + const Spacer(), + Column( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), + margin: + const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), + child: Text( + module.titre, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + overflow: TextOverflow.clip, + ), + ), + SizedBox( + width: 200, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progress, + minHeight: 6, + color: const Color.fromARGB(255, 90, 230, 220), + backgroundColor: const Color.fromARGB(255, 175, 240, 235), + ), + ), + ) + ], + ), + const Spacer() + ], + ), + ), + ); +} + +// Header affichant l'avancement dans le cours de l'utilisateur +SizedBox headerAvancement() { + return SizedBox( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), + margin: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: const Color.fromARGB(255, 219, 218, 215), + ), + child: Row( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), + margin: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), + child: const Column( + children: [ + Text( + "Votre Avancement", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), + ), + const Spacer(), + Stack( + alignment: Alignment.center, + children: [ + const Icon( + Icons.bookmark, + size: 140, + color: Color.fromARGB(255, 3, 47, 122), + ), + FutureBuilder( + future: ListModuleViewModel().getProgressionGlobale(), + builder: (context, snapshot) { + String progress = ""; + if (snapshot.hasData) { + progress = snapshot.data.toString(); + } + + return Text( + "$progress%", + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ); + }, + ), + ], + ), + ], + ), + ), + ); +} diff --git a/lib/ui/ModuleSelectionne.dart b/lib/ui/module_selectionne.dart similarity index 59% rename from lib/ui/ModuleSelectionne.dart rename to lib/ui/module_selectionne.dart index d606c3c..94c57fd 100644 --- a/lib/ui/ModuleSelectionne.dart +++ b/lib/ui/module_selectionne.dart @@ -1,42 +1,36 @@ import 'package:flutter/material.dart'; -import 'package:seriouse_game/models/cours.dart'; -import 'package:seriouse_game/models/module.dart'; +import 'package:factoscope/models/cours.dart'; +import 'package:factoscope/models/module.dart'; //Singleton représentant le module sélectionné par l'utilisateur et les données qu'il contient. class ModuleSelectionne with ChangeNotifier { + Module moduleSelectionne = + Module(titre: "titre", urlImg: "", description: "description", id: 1); - Module moduleSelectionne = Module(titre: "titre",urlImg: "", description: "description",id: 1); + static final ModuleSelectionne instance = ModuleSelectionne._internal(); - static final ModuleSelectionne instance = ModuleSelectionne._internal() ; - - //Liste contenant les cours d'un module - List coursDuModule = List.empty() ; - - static ModuleSelectionne getInstance(){ + // Liste contenant les cours d'un module + List coursDuModule = List.empty(); + static ModuleSelectionne getInstance() { return instance; - } factory ModuleSelectionne() { return instance; } - ModuleSelectionne._internal(); //Permet de modifier le module selectionné tout en avertissant les listeners - void changeModule(Module module){ - + void changeModule(Module module) { moduleSelectionne = module; notifyListeners(); - } //Permet de mettre à jour la liste des cours du module - void updateListModule(List listCours){ + void updateListModule(List listCours) { coursDuModule = listCours; notifyListeners(); } - -} \ No newline at end of file +} diff --git a/lib/ui/PageAttente.dart b/lib/ui/page_attente.dart similarity index 100% rename from lib/ui/PageAttente.dart rename to lib/ui/page_attente.dart diff --git a/lib/ui/validation_view.dart b/lib/ui/validation_view.dart new file mode 100644 index 0000000..f1ace22 --- /dev/null +++ b/lib/ui/validation_view.dart @@ -0,0 +1,260 @@ +import 'package:flutter/material.dart'; +import 'package:factoscope/ui/QCM/qcm_officiel_view.dart'; +import 'package:factoscope/ui/cours_selectionne.dart'; // AJOUT + +class ValidationView extends StatelessWidget { + const ValidationView({super.key}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 20), + + // Icône principale + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 3, 47, 122).withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.verified, + size: 100, + color: Color.fromARGB(255, 3, 47, 122), + ), + ), + const SizedBox(height: 32), + + const Text( + "Test de Validation", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + const Text( + "Obtenez votre validation officielle", + style: TextStyle( + fontSize: 18, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + + // Carte d'information + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color.fromARGB(255, 3, 47, 122).withValues(alpha: 0.2), + width: 2, + ), + ), + child: Column( + children: [ + _buildInfoRow( + Icons.stars, + "Score requis", + "100%", + const Color.fromRGBO(252, 179, 48, 1), + ), + const Divider(height: 32), + + _buildInfoRow( + Icons.quiz, + "Type de test", + "QCM officiel", + const Color.fromARGB(255, 3, 47, 122), + ), + ], + ), + ), + const SizedBox(height: 32), + + // Règles du test + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color.fromRGBO(252, 179, 48, 1).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color.fromRGBO(252, 179, 48, 1), + width: 2, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon( + Icons.rule, + color: Color.fromRGBO(252, 179, 48, 1), + size: 28, + ), + SizedBox(width: 12), + Text( + "Règles importantes", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + _buildRule("📚", "Complétez tous les modules avant de passer le test"), + _buildRule("✅", "Vous devez obtenir 100% de bonnes réponses"), + _buildRule("⏱️", "Le test se fera sous 30 minutes"), + _buildRule("🏆", "Une fois validé, vous recevrez votre certification"), + ], + ), + ), + const SizedBox(height: 40), + + // Message d'encouragement + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 90, 230, 220).withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon( + Icons.lightbulb, + color: Color.fromARGB(255, 3, 47, 122), + size: 30, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + "Conseil : Révisez bien tous les cours avant de commencer le test officiel !", + style: TextStyle( + fontSize: 16, + color: Colors.grey[800], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 40), + + // Bouton ACTIVÉ + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + final cours = CoursSelectionne.instance.cours; // AJOUT + + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => QCMOfficielView(cours: cours), // AJOUT + ), + ); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 20, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + backgroundColor: const Color.fromARGB(255, 3, 47, 122), + ), + child: const Text( + "Commencer", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + Widget _buildInfoRow(IconData icon, String label, String value, Color color) { + return Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: color, + size: 28, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildRule(String icon, String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + icon, + style: const TextStyle(fontSize: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: const TextStyle(fontSize: 16), + ), + ), + ], + ), + ); + } +} diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 868f176..97e1775 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "seriouse_game") +set(BINARY_NAME "factoscope") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.seriouse_game") +set(APPLICATION_ID "com.example.factoscope") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/linux/my_application.cc b/linux/my_application.cc index cb86bb3..479b556 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "seriouse_game"); + gtk_header_bar_set_title(header_bar, "factoscope"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "seriouse_game"); + gtk_window_set_title(window, "factoscope"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 23c1b48..025ae28 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* seriouse_game.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "seriouse_game.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* factoscope.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "factoscope.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -131,7 +131,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* seriouse_game.app */, + 33CC10ED2044A3C60003C045 /* factoscope.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -217,7 +217,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* seriouse_game.app */; + productReference = 33CC10ED2044A3C60003C045 /* factoscope.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -388,7 +388,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.example.projetCollective.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/seriouse_game.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/seriouse_game"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/factoscope.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/factoscope"; }; name = Debug; }; @@ -402,7 +402,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.example.projetCollective.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/seriouse_game.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/seriouse_game"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/factoscope.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/factoscope"; }; name = Release; }; @@ -416,7 +416,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.example.projetCollective.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/seriouse_game.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/seriouse_game"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/factoscope.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/factoscope"; }; name = Profile; }; @@ -461,7 +461,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -543,7 +543,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -593,7 +593,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 23ddced..a219714 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -59,13 +59,14 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> @@ -82,7 +83,7 @@ diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 8e02df2..b3c1761 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index a923ad9..48b95f3 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = seriouse_game +PRODUCT_NAME = factoscope // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.example.projetCollective diff --git a/pubspec.lock b/pubspec.lock index d48f874..602d736 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: @@ -13,74 +21,74 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" audioplayers: dependency: "direct main" description: name: audioplayers - sha256: a5341380a4f1d3a10a4edde5bb75de5127fe31e0faa8c4d860e64d2f91ad84c7 + sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4" url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "6.5.1" audioplayers_android: dependency: transitive description: name: audioplayers_android - sha256: f8c90823a45b475d2c129f85bbda9c029c8d4450b172f62e066564c6e170f69a + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.1" audioplayers_darwin: dependency: transitive description: name: audioplayers_darwin - sha256: "405cdbd53ebdb4623f1c5af69f275dad4f930ce895512d5261c07cd95d23e778" + sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.0" audioplayers_linux: dependency: transitive description: name: audioplayers_linux - sha256: "7e0d081a6a527c53aef9539691258a08ff69a7dc15ef6335fbea1b4b03ebbef0" + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" audioplayers_platform_interface: dependency: transitive description: name: audioplayers_platform_interface - sha256: "77e5fa20fb4a64709158391c75c1cca69a481d35dc879b519e350a05ff520373" + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.1.1" audioplayers_web: dependency: transitive description: name: audioplayers_web - sha256: bd99d8821114747682a2be0adcdb70233d4697af989b549d3a20a0f49f6c9b13 + sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.1.1" audioplayers_windows: dependency: transitive description: name: audioplayers_windows - sha256: "871d3831c25cd2408ddc552600fd4b32fba675943e319a41284704ee038ad563" + sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: @@ -89,6 +97,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -109,10 +133,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" csslib: dependency: transitive description: @@ -141,10 +165,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -166,6 +190,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -178,10 +210,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -212,18 +244,18 @@ packages: dependency: transitive description: name: html - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5" + version: "0.15.6" http: - dependency: transitive + dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.6.0" http_parser: dependency: transitive description: @@ -232,6 +264,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -296,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: "direct main" description: @@ -324,18 +380,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.22" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -364,10 +420,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.1" platform: dependency: transitive description: @@ -384,6 +440,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" sky_engine: dependency: transitive description: flutter @@ -393,50 +465,42 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" + version: "1.10.1" sqflite: dependency: "direct main" description: name: sqflite - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" url: "https://pub.dev" source: hosted - version: "2.5.4+6" + version: "2.5.6" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" url: "https://pub.dev" source: hosted - version: "2.4.1+1" + version: "2.4.2" sqflite_platform_interface: dependency: transitive description: @@ -465,26 +529,26 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.4.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: @@ -505,18 +569,18 @@ packages: dependency: transitive description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.2" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -529,10 +593,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.19" vector_math: dependency: transitive description: @@ -545,50 +609,50 @@ packages: dependency: "direct main" description: name: video_player - sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" + sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" url: "https://pub.dev" source: hosted - version: "2.9.5" + version: "2.10.1" video_player_android: dependency: transitive description: name: video_player_android - sha256: ae7d4f1b41e3ac6d24dd9b9d5d6831b52d74a61bdd90a7a6262a33d8bb97c29a + sha256: "3f7ef3fb7b29f510e58f4d56b6ffbc3463b1071f2cf56e10f8d25f5b991ed85b" url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.8.21" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "84b4752745eeccb6e75865c9aab39b3d28eb27ba5726d352d45db8297fbd75bc" + sha256: "6bced1739cf1f96f03058118adb8ac0dd6f96aa1a1a6e526424ab92fd2a6a77d" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.8.7" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 + sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.6.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476" + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.4.0" vm_service: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.0.2" web: dependency: transitive description: @@ -609,10 +673,18 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "3.1.3" sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index bb09add..720dfb3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,104 +1,48 @@ -name: seriouse_game +name: factoscope description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: ^3.5.3 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter + + provider: ^6.1.5 sqflite: ^2.0.0+4 path: ^1.8.0 - get_it: ^7.6.0 - flutter_svg: ^2.0.7 - - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. + get_it: ^9.2.1 + flutter_svg: ^2.2.4 + http: ^1.1.0 cupertino_icons: ^1.0.8 video_player: ^2.9.2 audioplayers: ^6.2.0 - go_router: ^14.8.1 + go_router: ^17.1.0 + path_provider: ^2.1.2 + flutter_dotenv: ^6.0.0 dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^6.0.0 + flutter_launcher_icons: "^0.14.4" - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^4.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/icon/icon_android.png" + min_sdk_android: 21 -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: assets: - - lib/data/AppData/ - - lib/data/AppData/CharteFactoscope/ - - lib/data/AppData/Module1/Cours1/ - - lib/data/AppData/Module1/Cours2/ - - lib/data/ArchiveBestPattern2QCM/ - - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package + - .env + - lib/assets/ + - lib/data/AppData/ + - lib/data/AppData/Module1/ + - lib/data/AppData/Module2/ + - lib/data/AppData/Module3/ diff --git a/test/widget_test.dart b/test/widget_test.dart index adcffe0..56bba64 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:seriouse_game/main.dart'; +import 'package:factoscope/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { diff --git a/web/index.html b/web/index.html index f450736..9f0b0ac 100644 --- a/web/index.html +++ b/web/index.html @@ -23,13 +23,13 @@ - + - seriouse_game + factoscope diff --git a/web/manifest.json b/web/manifest.json index cf5e23d..40e4008 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "seriouse_game", - "short_name": "seriouse_game", + "name": "factoscope", + "short_name": "factoscope", "start_url": ".", "display": "standalone", "background_color": "#0175C2", diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 5fc72de..8c75434 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(seriouse_game LANGUAGES CXX) +project(factoscope LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "seriouse_game") +set(BINARY_NAME "factoscope") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index c45dd48..42e23c9 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "seriouse_game" "\0" + VALUE "FileDescription", "factoscope" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "seriouse_game" "\0" + VALUE "InternalName", "factoscope" "\0" VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "seriouse_game.exe" "\0" - VALUE "ProductName", "seriouse_game" "\0" + VALUE "OriginalFilename", "factoscope.exe" "\0" + VALUE "ProductName", "factoscope" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 2c32659..22788d3 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"seriouse_game", origin, size)) { + if (!window.Create(L"factoscope", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true);