Initial commit du projet Flutter

This commit is contained in:
2025-12-01 10:56:37 +01:00
commit 8a728a612e
162 changed files with 33799 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: ios
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
# wtpe
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.wtpe"
compileSdk = flutter.compileSdkVersion
ndkVersion ="27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.wtpe"
// 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
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,60 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.wtpe">
<!-- Permissions nécessaires pour l'application -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Fusion des deux <queries> -->
<queries>
<package android:name="com.whatsapp" />
<package android:name="com.whatsapp.w4b" />
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.settings.WIFI_SETTINGS" />
</intent>
<intent>
<action android:name="android.settings.SETTINGS" />
</intent>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
<application
android:label="wtpe"
android:name="${applicationName}"
android:testOnly="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@@ -0,0 +1,25 @@
package com.example.wtpe
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action ||
"android.intent.action.QUICKBOOT_POWERON" == intent.action) {
Log.d("BootReceiver", "WTPE - Démarrage détecté, lancement de l'application")
val startIntent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
context.startActivity(startIntent)
}
}
}

View File

@@ -0,0 +1,29 @@
package com.example.wtpe
import android.app.admin.DeviceAdminReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
class DeviceAdminReceiver : DeviceAdminReceiver() {
override fun onEnabled(context: Context, intent: Intent) {
super.onEnabled(context, intent)
Log.d("DeviceAdmin", "WTPE Device Admin activé")
}
override fun onDisabled(context: Context, intent: Intent) {
super.onDisabled(context, intent)
Log.d("DeviceAdmin", "WTPE Device Admin désactivé")
}
override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) {
super.onLockTaskModeEntering(context, intent, pkg)
Log.d("DeviceAdmin", "WTPE - Entrée en mode Lock Task: $pkg")
}
override fun onLockTaskModeExiting(context: Context, intent: Intent) {
super.onLockTaskModeExiting(context, intent)
Log.d("DeviceAdmin", "WTPE - Sortie du mode Lock Task")
}
}

View File

@@ -0,0 +1,617 @@
package com.example.wtpe
import android.app.ActivityManager
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.PixelFormat
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.wortis.agent/settings"
private val KIOSK_CHANNEL = "kiosk_mode"
private lateinit var devicePolicyManager: DevicePolicyManager
private lateinit var compName: ComponentName
private var overlayView: View? = null
private var isKioskModeActive = true
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
devicePolicyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
compName = ComponentName(this, DeviceAdminReceiver::class.java)
// Configuration du mode kiosque au démarrage
enableKioskMode()
// Ajoutez cette méthode au canal CHANNEL dans configureFlutterEngine :
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
// ... vos autres méthodes ...
"forceRestartKiosk" -> {
// Méthode pour forcer le retour en mode kiosque
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
enableAdvancedKioskMode()
} else {
enableKioskMode()
}
result.success("Kiosk mode restarted")
}
else -> {
result.notImplemented()
}
}
}
// Canal principal pour les paramètres système
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"openWifiSettings" -> {
openWifiSettings()
result.success("WiFi settings opened")
}
"openSettings" -> {
openSettings()
result.success("Settings opened")
}
// ============= AJOUTER CES MÉTHODES =============
"enableKioskMode" -> {
enableKioskMode()
result.success("Kiosk mode enabled")
}
"exitKioskMode" -> {
exitKioskMode()
result.success("Kiosk mode disabled")
}
// ============= FIN AJOUT =============
"setBrightness" -> {
val brightness = call.argument<Double>("brightness") ?: 0.5
setBrightness(brightness.toFloat())
result.success("Brightness set to ${(brightness * 100).toInt()}%")
}
"getSystemBrightness" -> {
val brightness = getSystemBrightness()
result.success(brightness.toDouble())
}
"openWhatsApp" -> {
val opened = openWhatsApp()
if (opened) {
result.success("WhatsApp opened")
} else {
result.error("WHATSAPP_ERROR", "WhatsApp not installed or error opening", null)
}
}
else -> {
result.notImplemented()
}
}
}
// Canal pour le mode kiosque avancé
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KIOSK_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"enableKioskMode" -> {
enableAdvancedKioskMode()
result.success(null)
}
"disableKioskMode" -> {
disableAdvancedKioskMode()
result.success(null)
}
"isDeviceOwner" -> {
result.success(devicePolicyManager.isDeviceOwnerApp(packageName))
}
"requestDeviceAdmin" -> {
requestDeviceAdmin()
result.success(null)
}
"hideNavigationBar" -> {
hideSystemUI()
result.success(null)
}
"enableStatusBarExpansion" -> {
enableStatusBarExpansion(call.argument<Boolean>("enable") ?: false)
result.success(null)
}
"preventStatusBarExpansion" -> {
preventStatusBarExpansion()
result.success(null)
}
"exitKioskMode" -> {
exitKioskMode()
result.success("Kiosk mode disabled")
}
else -> {
result.notImplemented()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Configuration de la fenêtre pour le mode kiosque avancé
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_FULLSCREEN or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_SECURE
)
// Masquer immédiatement tous les éléments système
hideSystemUI()
preventStatusBarExpansion()
// Créer un overlay pour bloquer les gestes système
createSystemOverlay()
// Activer le mode kiosque avancé si on est Device Owner
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
enableAdvancedKioskMode()
} else {
enableKioskMode()
}
}
// Modifiez onResume pour réactiver automatiquement le mode kiosque :
override fun onResume() {
super.onResume()
// Délai pour permettre à l'utilisateur de revenir depuis les paramètres
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
if (isKioskModeActive) {
hideSystemUI()
preventStatusBarExpansion()
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
// Vérifier si on doit relancer le Lock Task
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
if (!activityManager.isInLockTaskMode) {
enableAdvancedKioskMode()
android.util.Log.d("MainActivity", "Lock Task réactivé depuis onResume")
}
} else {
enableKioskMode()
}
}
}, 1000) // Délai de 1 seconde
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus && isKioskModeActive) {
hideSystemUI()
}
}
private fun createSystemOverlay() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
if (Settings.canDrawOverlays(this)) {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
// Créer un overlay invisible au-dessus de la barre de statut
overlayView = View(this)
overlayView?.setBackgroundColor(android.graphics.Color.TRANSPARENT)
val layoutParams = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LayoutParams(
LayoutParams.MATCH_PARENT,
getStatusBarHeight(),
LayoutParams.TYPE_APPLICATION_OVERLAY,
LayoutParams.FLAG_NOT_TOUCHABLE or
LayoutParams.FLAG_LAYOUT_IN_SCREEN or
LayoutParams.FLAG_FULLSCREEN,
PixelFormat.TRANSLUCENT
)
} else {
LayoutParams(
LayoutParams.MATCH_PARENT,
getStatusBarHeight(),
LayoutParams.TYPE_SYSTEM_OVERLAY,
LayoutParams.FLAG_NOT_TOUCHABLE or
LayoutParams.FLAG_LAYOUT_IN_SCREEN or
LayoutParams.FLAG_FULLSCREEN,
PixelFormat.TRANSLUCENT
)
}
layoutParams.gravity = Gravity.TOP
windowManager.addView(overlayView, layoutParams)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun getStatusBarHeight(): Int {
var result = 0
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
result = resources.getDimensionPixelSize(resourceId)
}
return result
}
private fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LOW_PROFILE
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}
// Configuration supplémentaire pour Android 11+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
}
// Masquer les barres système
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = android.graphics.Color.TRANSPARENT
window.navigationBarColor = android.graphics.Color.TRANSPARENT
}
}
private fun enableKioskMode() {
try {
isKioskModeActive = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
}
// Empêcher l'extinction de l'écran
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
// Masquer la barre de navigation pour les écrans avec encoche
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun enableAdvancedKioskMode() {
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
try {
isKioskModeActive = true
val packages = arrayOf(packageName)
devicePolicyManager.setLockTaskPackages(compName, packages)
startLockTask()
// Désactiver la barre de statut si Device Owner
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
devicePolicyManager.setStatusBarDisabled(compName, true)
}
// Appliquer aussi les configurations de base
enableKioskMode()
} catch (e: Exception) {
e.printStackTrace()
// Fallback vers le mode kiosque de base
enableKioskMode()
}
} else {
// Si pas Device Owner, utiliser le mode kiosque de base
enableKioskMode()
}
}
private fun disableAdvancedKioskMode() {
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
try {
stopLockTask()
// Réactiver la barre de statut
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
devicePolicyManager.setStatusBarDisabled(compName, false)
}
exitKioskMode()
} catch (e: Exception) {
e.printStackTrace()
exitKioskMode()
}
} else {
exitKioskMode()
}
}
private fun exitKioskMode() {
try {
isKioskModeActive = false
// Restaurer l'UI normale
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
// Permettre l'extinction automatique de l'écran
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun setBrightness(brightness: Float) {
try {
runOnUiThread {
val layoutParams = window.attributes
layoutParams.screenBrightness = brightness.coerceIn(0.0f, 1.0f)
window.attributes = layoutParams
}
// Log pour debug
android.util.Log.d("MainActivity", "Luminosité réglée à: ${(brightness * 100).toInt()}%")
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Erreur réglage luminosité: ${e.message}")
e.printStackTrace()
}
}
private fun getSystemBrightness(): Float {
return try {
val currentBrightness = window.attributes.screenBrightness
if (currentBrightness >= 0) {
android.util.Log.d("MainActivity", "Luminosité actuelle: ${(currentBrightness * 100).toInt()}%")
currentBrightness
} else {
0.5f
}
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Erreur lecture luminosité: ${e.message}")
0.5f
}
}
private fun openWhatsApp(): Boolean {
return try {
// Sortir du Lock Task si on est Device Owner
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
try {
stopLockTask()
android.util.Log.d("MainActivity", "Lock Task arrêté pour WhatsApp")
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Erreur arrêt Lock Task: ${e.message}")
}
}
// Désactiver le mode kiosque temporairement
exitKioskMode()
val packageManager = packageManager
// Essayer WhatsApp Business EN PREMIER
var intent = packageManager.getLaunchIntentForPackage("com.whatsapp.w4b")
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
android.util.Log.d("MainActivity", "WhatsApp Business ouvert")
return true
}
// Fallback vers WhatsApp normal
intent = packageManager.getLaunchIntentForPackage("com.whatsapp")
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
android.util.Log.d("MainActivity", "WhatsApp normal ouvert")
return true
}
// Si aucun WhatsApp installé, ouvrir le Play Store pour WhatsApp Business
try {
val playStoreIntent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("market://details?id=com.whatsapp.w4b")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(playStoreIntent)
return true
} catch (e: Exception) {
val webIntent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("https://play.google.com/store/apps/details?id=com.whatsapp.w4b")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(webIntent)
return true
}
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Erreur ouverture WhatsApp: ${e.message}")
e.printStackTrace()
// Réactiver le mode kiosque en cas d'erreur
enableKioskMode()
false
}
}
private fun openWifiSettings() {
try {
// Sortir du Lock Task si on est Device Owner
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
try {
stopLockTask()
android.util.Log.d("MainActivity", "Lock Task arrêté pour WiFi")
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Erreur arrêt Lock Task: ${e.message}")
}
}
// Désactiver le mode kiosque temporairement
exitKioskMode()
// Essayer d'ouvrir directement les paramètres WiFi
val intent = Intent(Settings.ACTION_WIFI_SETTINGS).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
startActivity(intent)
android.util.Log.d("MainActivity", "Paramètres WiFi ouverts")
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Erreur ouverture WiFi: ${e.message}")
e.printStackTrace()
// Fallback vers les paramètres généraux
openSettings()
}
}
private fun openSettings() {
try {
// Sortir du Lock Task si on est Device Owner
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
try {
stopLockTask()
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Erreur arrêt Lock Task: ${e.message}")
}
}
// Désactiver le mode kiosque temporairement
exitKioskMode()
val intent = Intent(Settings.ACTION_SETTINGS).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
// Réactiver le mode kiosque en cas d'erreur
enableKioskMode()
}
}
private fun requestDeviceAdmin() {
val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN).apply {
putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, compName)
putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
"Cette application nécessite des privilèges d'administrateur pour fonctionner en mode kiosque.")
}
startActivityForResult(intent, 1)
}
private fun preventStatusBarExpansion() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
// Empêcher l'expansion de la barre de statut
val service = getSystemService(Context.STATUS_BAR_SERVICE)
val statusbarManager = Class.forName("android.app.StatusBarManager")
val expand = statusbarManager.getMethod("collapsePanels")
expand.invoke(service)
// Désactiver les notifications pour cette app si Device Owner
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
devicePolicyManager.setStatusBarDisabled(compName, true)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun enableStatusBarExpansion(enable: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && devicePolicyManager.isDeviceOwnerApp(packageName)) {
try {
devicePolicyManager.setStatusBarDisabled(compName, !enable)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
// Désactiver le bouton retour en mode kiosque uniquement
if (isKioskModeActive) {
// Permettre le bouton retour pour la navigation dans l'app en mode kiosque avancé
if (devicePolicyManager.isDeviceOwnerApp(packageName)) {
super.onBackPressed()
}
// Sinon, ne rien faire pour empêcher la sortie
} else {
// Permettre la navigation normale quand pas en mode kiosque
super.onBackPressed()
}
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// Ne forcer le retour que si en mode kiosque
if (isKioskModeActive) {
try {
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun onPause() {
super.onPause()
// Ramener immédiatement l'app au premier plan en mode kiosque avancé seulement
if (isKioskModeActive && devicePolicyManager.isDeviceOwnerApp(packageName)) {
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)
}
}
override fun onDestroy() {
super.onDestroy()
// Supprimer l'overlay si il existe
if (overlayView != null) {
try {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.removeView(overlayView)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies>
<limit-password />
<watch-login />
<reset-password />
<force-lock />
<wipe-data />
<expire-password />
<encrypted-storage />
<disable-camera />
<disable-keyguard />
</uses-policies>
</device-admin>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

21
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

View File

@@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

3
devtools_options.yaml Normal file
View File

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

34
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Normal file
View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

60
ios/Podfile.lock Normal file
View File

@@ -0,0 +1,60 @@
PODS:
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- permission_handler_apple (9.3.0):
- Flutter
- ReachabilitySwift (5.2.4)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- wifi_iot (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wifi_iot (from `.symlinks/plugins/wifi_iot/ios`)
SPEC REPOS:
trunk:
- ReachabilitySwift
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wifi_iot:
:path: ".symlinks/plugins/wifi_iot/ios"
SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wifi_iot: b5aafd6f9b52f8a357383a1deabab45f31cd602d
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,746 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B895CECD046E00290D6217E1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7CA508A0C6BB08473D87CA9 /* Pods_RunnerTests.framework */; };
FF27D0F8433199F9DD19C699 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 360B028F110497A0F038EB3A /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0235224B225D42AA7D06F707 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
09F3BFBA64DB728ECE3E15FC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1FF10AB1C5D17217EA36132A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
360B028F110497A0F038EB3A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
497EB272EB768463661EEF55 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
5BD8746531B8353A4EF87CAF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
79C528EB1D9C6A1E837111AD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E7CA508A0C6BB08473D87CA9 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FF27D0F8433199F9DD19C699 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D00D2F41D8147D197DD14523 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B895CECD046E00290D6217E1 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
2D2A65B32617AF073B1312DC /* Pods */ = {
isa = PBXGroup;
children = (
5BD8746531B8353A4EF87CAF /* Pods-Runner.debug.xcconfig */,
1FF10AB1C5D17217EA36132A /* Pods-Runner.release.xcconfig */,
09F3BFBA64DB728ECE3E15FC /* Pods-Runner.profile.xcconfig */,
497EB272EB768463661EEF55 /* Pods-RunnerTests.debug.xcconfig */,
79C528EB1D9C6A1E837111AD /* Pods-RunnerTests.release.xcconfig */,
0235224B225D42AA7D06F707 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
2D2A65B32617AF073B1312DC /* Pods */,
DC831A6E4073695EE3E6CB36 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
DC831A6E4073695EE3E6CB36 /* Frameworks */ = {
isa = PBXGroup;
children = (
360B028F110497A0F038EB3A /* Pods_Runner.framework */,
E7CA508A0C6BB08473D87CA9 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
601EA6CA18A9EAE41CD37F6E /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
D00D2F41D8147D197DD14523 /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
0972CA5AFEBA5C5A79667FE7 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
96AB7D58981E6B28931F38B0 /* [CP] Embed Pods Frameworks */,
2969FEDEC51FFCFF0A7A46D5 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0972CA5AFEBA5C5A79667FE7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
2969FEDEC51FFCFF0A7A46D5 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
601EA6CA18A9EAE41CD37F6E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
96AB7D58981E6B28931F38B0 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.wtpe;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 497EB272EB768463661EEF55 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.wtpe.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 79C528EB1D9C6A1E837111AD /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.wtpe.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 0235224B225D42AA7D06F707 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.wtpe.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.wtpe;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.wtpe;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Wtpe</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>wtpe</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import '../models/service_model.dart';
import '../services/wortis_api_service.dart';
class ServicesController extends ChangeNotifier {
final WortisApiService _apiService = WortisApiService();
List<WortisService> _allServices = [];
Map<String, List<WortisService>> _servicesBySector = {};
bool _isLoading = false;
String? _error;
String _searchQuery = '';
String _selectedSector = 'Tous';
// Ordre de priorité des secteurs (index 0 = plus haute priorité)
static const List<String> _sectorPriority = [
'Service Public',
// Ajoutez d'autres secteurs selon vos besoins
];
// Getters
List<WortisService> get allServices => _allServices;
Map<String, List<WortisService>> get servicesBySector => _servicesBySector;
bool get isLoading => _isLoading;
String? get error => _error;
String get searchQuery => _searchQuery;
String get selectedSector => _selectedSector;
List<String> get sectors => ['Tous', ..._servicesBySector.keys];
// Méthode pour obtenir la priorité d'un secteur
int _getSectorPriority(String sector) {
final normalizedSector = sector.toLowerCase();
for (int i = 0; i < _sectorPriority.length; i++) {
if (_sectorPriority[i].toLowerCase() == normalizedSector) {
return i;
}
}
return _sectorPriority
.length; // Secteurs non listés ont la priorité la plus basse
}
// Méthode pour trier les services par secteur prioritaire et rang
List<WortisService> _sortServicesByPriority(List<WortisService> services) {
final List<WortisService> sortedServices = List.from(services);
sortedServices.sort((a, b) {
// D'abord, comparer par priorité de secteur
final priorityA = _getSectorPriority(a.secteurActivite);
final priorityB = _getSectorPriority(b.secteurActivite);
if (priorityA != priorityB) {
return priorityA.compareTo(priorityB);
}
// Si même secteur, trier par rang
if (a.rang != null && b.rang != null) {
return a.rang!.compareTo(b.rang!);
}
// Si seulement le premier a un rang, il passe en premier
else if (a.rang != null && b.rang == null) {
return -1;
}
// Si seulement le deuxième a un rang, il passe en premier
else if (a.rang == null && b.rang != null) {
return 1;
}
// Si aucun n'a de rang, trier par nom alphabétiquement
else {
return a.name.compareTo(b.name);
}
});
return sortedServices;
}
// Services filtrés selon la recherche et le secteur avec tri par priorité
List<WortisService> get filteredServices {
List<WortisService> services;
if (_selectedSector == 'Tous') {
services = _allServices;
} else {
services = _servicesBySector[_selectedSector] ?? [];
}
if (_searchQuery.isNotEmpty) {
services =
services.where((service) {
final name = service.name.toLowerCase();
final description = service.description.toLowerCase();
final sector = service.secteurActivite.toLowerCase();
final query = _searchQuery.toLowerCase();
return name.contains(query) ||
description.contains(query) ||
sector.contains(query);
}).toList();
}
// Appliquer le tri par priorité de secteur et rang
return _sortServicesByPriority(services);
}
// Charger les services depuis l'API
Future<void> loadServices() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_allServices = await _apiService.getServices();
// Trier tous les services par priorité dès le chargement
_allServices = _sortServicesByPriority(_allServices);
_servicesBySector = _apiService.groupServicesBySector(_allServices);
// Trier les services dans chaque secteur
_servicesBySector.forEach((key, value) {
_servicesBySector[key] = _sortServicesByPriority(value);
});
_error = null;
} catch (e) {
_error = e.toString();
_allServices = [];
_servicesBySector = {};
} finally {
_isLoading = false;
notifyListeners();
}
}
// Rafraîchir les services
Future<void> refreshServices() async {
await loadServices();
}
// Mettre à jour la recherche
void updateSearchQuery(String query) {
_searchQuery = query;
notifyListeners();
}
// Changer le secteur sélectionné
void selectSector(String sector) {
_selectedSector = sector;
notifyListeners();
}
// Vider la recherche
void clearSearch() {
_searchQuery = '';
notifyListeners();
}
// Obtenir un service par ID
WortisService? getServiceById(String id) {
try {
return _allServices.firstWhere((service) => service.id == id);
} catch (e) {
return null;
}
}
// Obtenir les services actifs uniquement avec tri par priorité
List<WortisService> get activeServices {
final active = _allServices.where((service) => service.status).toList();
return _sortServicesByPriority(active);
}
// Obtenir le nombre de services par secteur
Map<String, int> get servicesCountBySector {
Map<String, int> count = {};
for (var entry in _servicesBySector.entries) {
count[entry.key] = entry.value.length;
}
return count;
}
// Méthode pour modifier l'ordre de priorité des secteurs si nécessaire
static List<String> get sectorPriority => List.from(_sectorPriority);
}

1340
lib/index.html Normal file

File diff suppressed because it is too large Load Diff

1134
lib/main.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
// Dans ../models/service_model.dart
import 'package:flutter/material.dart';
class WortisService {
final String id;
final String name;
final String description;
final String secteurActivite;
final bool status;
final String? icon;
final String? banner;
final String? linkView;
final String? typeService;
final String? boutonList;
final String? buttonService;
final String? error400;
final String? error500;
final String? titeDescription;
final int? rang; // ← Ajoutez cette ligne
WortisService({
required this.id,
required this.name,
required this.description,
required this.secteurActivite,
required this.status,
this.icon,
this.banner,
this.linkView,
this.typeService,
this.boutonList,
this.buttonService,
this.error400,
this.error500,
this.titeDescription,
this.rang, // ← Ajoutez cette ligne
});
factory WortisService.fromJson(Map<String, dynamic> json) {
return WortisService(
id: json['_id'] ?? '',
name: json['name'] ?? '',
description: json['description'] ?? '',
secteurActivite: json['SecteurActivite'] ?? '',
status: json['status'] ?? false,
icon: json['icon'],
banner: json['banner'],
linkView: json['link_view'],
typeService: json['Type_Service'],
boutonList: json['bouton_list'],
buttonService: json['button_service'],
error400: json['error_400'],
error500: json['error_500'],
titeDescription: json['tite_description'],
rang: json['rang'] as int?, // ← Ajoutez cette ligne
);
}
Map<String, dynamic> toJson() {
return {
'_id': id,
'name': name,
'description': description,
'SecteurActivite': secteurActivite,
'status': status,
'icon': icon,
'banner': banner,
'link_view': linkView,
'Type_Service': typeService,
'bouton_list': boutonList,
'button_service': buttonService,
'error_400': error400,
'error_500': error500,
'tite_description': titeDescription,
'rang': rang, // ← Ajoutez cette ligne
};
}
// Getter pour l'icône Flutter (vous pouvez garder votre logique existante)
IconData get flutterIcon {
switch (icon?.toLowerCase()) {
case 'movie':
return Icons.movie;
case 'car':
return Icons.directions_car;
case 'shopping':
return Icons.shopping_cart;
case 'food':
return Icons.restaurant;
case 'hotel':
return Icons.hotel;
case 'transport':
return Icons.directions_bus;
case 'health':
return Icons.local_hospital;
case 'education':
return Icons.school;
case 'finance':
return Icons.account_balance;
default:
return Icons.miscellaneous_services;
}
}
// Getter pour la couleur du secteur (vous pouvez garder votre logique existante)
Color get sectorColor {
switch (secteurActivite.toLowerCase()) {
case 'billetterie':
return Color(0xFF6B46C1);
case 'transport':
return Color(0xFF059669);
case 'restauration':
return Color(0xFFDC2626);
case 'hébergement':
return Color(0xFF2563EB);
case 'finance':
return Color(0xFF7C2D12);
case 'santé':
return Color(0xFF0891B2);
case 'éducation':
return Color(0xFF7C3AED);
default:
return Color(0xFF6B7280);
}
}
// Méthode copyWith pour créer une copie avec des modifications
WortisService copyWith({
String? id,
String? name,
String? description,
String? secteurActivite,
bool? status,
String? icon,
String? banner,
String? linkView,
String? typeService,
String? boutonList,
String? buttonService,
String? error400,
String? error500,
String? titeDescription,
int? rang,
}) {
return WortisService(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
secteurActivite: secteurActivite ?? this.secteurActivite,
status: status ?? this.status,
icon: icon ?? this.icon,
banner: banner ?? this.banner,
linkView: linkView ?? this.linkView,
typeService: typeService ?? this.typeService,
boutonList: boutonList ?? this.boutonList,
buttonService: buttonService ?? this.buttonService,
error400: error400 ?? this.error400,
error500: error500 ?? this.error500,
titeDescription: titeDescription ?? this.titeDescription,
rang: rang ?? this.rang,
);
}
@override
String toString() {
return 'WortisService(id: $id, name: $name, secteur: $secteurActivite, status: $status, rang: $rang)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is WortisService && other.id == id;
}
@override
int get hashCode => id.hashCode;
}

File diff suppressed because it is too large Load Diff

840
lib/pages/class.dart Normal file
View File

@@ -0,0 +1,840 @@
// lib/class/class.dart
import 'package:flutter/material.dart';
/// Gestionnaire de connectivité
class ConnectivityManager {
final BuildContext context;
ConnectivityManager(this.context);
/// Initialiser la connectivité
void get initConnectivity {
print('Initialisation de la connectivité');
}
}
/// Gestionnaire d'overlays personnalisés
class CustomOverlay {
static OverlayEntry? _currentOverlay;
/// Afficher un message d'erreur
static void showError(BuildContext context, {required String message}) {
hide();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.error, color: Colors.white, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
message,
style: TextStyle(color: Colors.white, fontSize: 14),
),
),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
duration: Duration(seconds: 4),
),
);
}
/// Afficher un message de succès
static void showSuccess(BuildContext context, {required String message}) {
hide();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.check_circle, color: Colors.white, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
message,
style: TextStyle(color: Colors.white, fontSize: 14),
),
),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
duration: Duration(seconds: 3),
),
);
}
/// Afficher un message d'information
static void showInfo(BuildContext context, {required String message}) {
hide();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.info_outline, color: Colors.white, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
message,
style: TextStyle(color: Colors.white, fontSize: 14),
),
),
],
),
backgroundColor: Color(0xFF006699),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
duration: Duration(seconds: 3),
),
);
}
/// Afficher un avertissement
static void showWarning(BuildContext context, {required String message}) {
hide();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.warning_amber, color: Colors.white, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
message,
style: TextStyle(color: Colors.white, fontSize: 14),
),
),
],
),
backgroundColor: Colors.orange,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
duration: Duration(seconds: 4),
),
);
}
/// Afficher un indicateur de chargement
static void showLoading(
BuildContext context, {
String message = 'Chargement...',
}) {
hide();
_currentOverlay = OverlayEntry(
builder:
(context) => Material(
color: Colors.black54,
child: Center(
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF006699),
),
),
SizedBox(height: 16),
Text(
message,
style: TextStyle(fontSize: 16, color: Colors.black87),
),
],
),
),
),
),
);
Overlay.of(context).insert(_currentOverlay!);
}
/// Masquer l'overlay
static void hide() {
if (_currentOverlay != null) {
_currentOverlay!.remove();
_currentOverlay = null;
}
}
}
/// Classe utilitaire pour les widgets personnalisés
class CustomWidgets {
/// Créer un champ de saisie stylé
static Widget buildStyledTextField({
required String label,
required String hint,
required IconData icon,
TextEditingController? controller,
bool obscureText = false,
String? Function(String?)? validator,
void Function(String)? onChanged,
bool readOnly = false,
VoidCallback? onTap,
TextInputType? keyboardType,
int? maxLength,
String? suffixText,
Widget? suffix,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF2C3E50),
),
),
SizedBox(height: 8),
TextFormField(
controller: controller,
validator: validator,
obscureText: obscureText,
readOnly: readOnly,
onTap: onTap,
onChanged: onChanged,
keyboardType: keyboardType,
maxLength: maxLength,
decoration: InputDecoration(
hintText: hint,
prefixIcon: Icon(icon, color: Color(0xFF006699)),
suffixText: suffixText,
suffix: suffix,
filled: true,
fillColor: Colors.white,
counterText: '',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Color(0xFFE0E7FF)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Color(0xFFE0E7FF)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Color(0xFF006699), width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.red),
),
),
),
],
);
}
/// Créer un bouton stylé
static Widget buildStyledButton({
required String text,
required VoidCallback onPressed,
bool isLoading = false,
Color? backgroundColor,
Color? textColor,
IconData? icon,
double? height,
double? width,
bool enabled = true,
}) {
return SizedBox(
width: width ?? double.infinity,
height: height ?? 48,
child: ElevatedButton(
onPressed: (isLoading || !enabled) ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? Color(0xFF006699),
foregroundColor: textColor ?? Colors.white,
elevation: 2,
disabledBackgroundColor: Colors.grey[300],
disabledForegroundColor: Colors.grey[600],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child:
isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 18),
SizedBox(width: 8),
],
Text(
text,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
/// ===== NOUVEAU: Badge de type de compte =====
static Widget buildAccountTypeBadge({
required bool isEnterprise,
double fontSize = 12,
EdgeInsets? padding,
}) {
return Container(
padding: padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color:
isEnterprise
? Color(0xFF8B5CF6).withOpacity(0.1)
: Color(0xFF006699).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color:
isEnterprise
? Color(0xFF8B5CF6).withOpacity(0.3)
: Color(0xFF006699).withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isEnterprise ? Icons.business : Icons.person,
color: isEnterprise ? Color(0xFF8B5CF6) : Color(0xFF006699),
size: fontSize + 2,
),
SizedBox(width: 6),
Text(
isEnterprise ? 'Entreprise' : 'Agent',
style: TextStyle(
color: isEnterprise ? Color(0xFF8B5CF6) : Color(0xFF006699),
fontSize: fontSize,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
/// ===== NOUVEAU: Carte d'information avec icône =====
static Widget buildInfoCard({
required String title,
required String value,
required IconData icon,
Color? iconColor,
Color? backgroundColor,
VoidCallback? onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: backgroundColor ?? Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: (iconColor ?? Color(0xFF006699)).withOpacity(0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: (iconColor ?? Color(0xFF006699)).withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: iconColor ?? Color(0xFF006699),
size: 24,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
if (onTap != null)
Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey[400]),
],
),
),
);
}
/// ===== NOUVEAU: Carte de statistique =====
static Widget buildStatCard({
required String title,
required String value,
required IconData icon,
Color? color,
String? subtitle,
}) {
final cardColor = color ?? Color(0xFF006699);
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [cardColor.withOpacity(0.8), cardColor],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: cardColor.withOpacity(0.3),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(icon, color: Colors.white, size: 32),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.trending_up, color: Colors.white, size: 16),
),
],
),
SizedBox(height: 12),
Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
title,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
if (subtitle != null) ...[
SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
),
),
],
],
),
);
}
/// ===== NOUVEAU: Séparateur avec texte =====
static Widget buildDividerWithText(String text) {
return Row(
children: [
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
text,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
],
);
}
/// ===== NOUVEAU: Liste vide stylée =====
static Widget buildEmptyState({
required String message,
String? subtitle,
IconData? icon,
VoidCallback? onRefresh,
}) {
return Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon ?? Icons.inbox_outlined,
size: 80,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
message,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
textAlign: TextAlign.center,
),
if (subtitle != null) ...[
SizedBox(height: 8),
Text(
subtitle,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
],
if (onRefresh != null) ...[
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRefresh,
icon: Icon(Icons.refresh),
label: Text('Actualiser'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF006699),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
],
],
),
),
);
}
/// ===== NOUVEAU: Dialog de confirmation =====
static Future<bool> showConfirmDialog({
required BuildContext context,
required String title,
required String message,
String confirmText = 'Confirmer',
String cancelText = 'Annuler',
Color? confirmColor,
IconData? icon,
}) async {
final result = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
if (icon != null) ...[
Icon(icon, color: confirmColor ?? Color(0xFF006699)),
SizedBox(width: 12),
],
Expanded(child: Text(title, style: TextStyle(fontSize: 18))),
],
),
content: Text(
message,
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
cancelText,
style: TextStyle(color: Colors.grey[600]),
),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: confirmColor ?? Color(0xFF006699),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(confirmText),
),
],
),
);
return result ?? false;
}
/// ===== NOUVEAU: Badge de statut =====
static Widget buildStatusBadge({
required String text,
required bool isActive,
double fontSize = 12,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color:
isActive
? Colors.green.withOpacity(0.1)
: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color:
isActive
? Colors.green.withOpacity(0.3)
: Colors.orange.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: isActive ? Colors.green : Colors.orange,
shape: BoxShape.circle,
),
),
SizedBox(width: 6),
Text(
text,
style: TextStyle(
color: isActive ? Colors.green[700] : Colors.orange[700],
fontSize: fontSize,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
/// ===== NOUVELLE CLASSE: Utilitaires pour entreprises =====
class EnterpriseWidgets {
/// Badge entreprise
static Widget buildEnterpriseBadge({
required String enterpriseName,
double fontSize = 12,
EdgeInsets? padding,
}) {
return Container(
padding: padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF8B5CF6).withOpacity(0.8), Color(0xFF8B5CF6)],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Color(0xFF8B5CF6).withOpacity(0.3),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.business, color: Colors.white, size: fontSize + 2),
SizedBox(width: 6),
Flexible(
child: Text(
enterpriseName,
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
/// Carte d'information entreprise
static Widget buildEnterpriseInfoCard({
required String enterpriseId,
required String enterpriseName,
required double balance,
int? totalMembers,
VoidCallback? onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF8B5CF6).withOpacity(0.8), Color(0xFF8B5CF6)],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Color(0xFF8B5CF6).withOpacity(0.3),
blurRadius: 15,
offset: Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.business, color: Colors.white, size: 32),
Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
enterpriseId,
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: 16),
Text(
enterpriseName,
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Solde disponible',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
SizedBox(height: 4),
Text(
'${balance.toStringAsFixed(0)} FCFA',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
if (totalMembers != null)
Column(
children: [
Icon(Icons.people, color: Colors.white, size: 24),
SizedBox(height: 4),
Text(
'$totalMembers',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
'Membres',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 10,
),
),
],
),
],
),
],
),
),
);
}
}

2695
lib/pages/dashboard.dart Normal file

File diff suppressed because it is too large Load Diff

2348
lib/pages/form_service.dart Normal file

File diff suppressed because it is too large Load Diff

1451
lib/pages/home.dart Normal file

File diff suppressed because it is too large Load Diff

946
lib/pages/login.dart Normal file
View File

@@ -0,0 +1,946 @@
// ===== lib/pages/login.dart AVEC SUPPORT ENTREPRISE =====
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:wtpe/widgets/wortis_logo.dart';
import '../main.dart';
import '../services/wortis_api_service.dart';
import 'role_navigator.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _agentIdController = TextEditingController();
final _pinController = TextEditingController();
bool _obscurePin = true;
bool _apiConnected = false;
bool _testingConnection = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
// Couleurs Wortis
static const Color primaryColor = Color(0xFF006699);
static const Color secondaryColor = Color(0xFF0088CC);
static const Color accentColor = Color(0xFFFF6B35);
static const Color backgroundColor = Color(0xFFF8FAFC);
static const Color surfaceColor = Colors.white;
static const Color errorColor = Color(0xFFE53E3E);
static const Color textPrimaryColor = Color(0xFF1A202C);
static const Color textSecondaryColor = Color(0xFF718096);
static const Color borderColor = Color(0xFFE2E8F0);
static const Color enterpriseColor = Color(0xFF8B5CF6);
@override
void initState() {
super.initState();
_setupAnimations();
_testApiConnection();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.0, 0.8, curve: Curves.easeOut),
),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, 0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.3, 1.0, curve: Curves.easeOut),
),
);
_animationController.forward();
}
Future<void> _testApiConnection() async {
setState(() {
_testingConnection = true;
});
final isConnected = await AuthApiService.testConnection();
if (mounted) {
setState(() {
_apiConnected = isConnected;
_testingConnection = false;
});
}
}
Future<void> _createTestUser() async {
String? nom = await _showCreateUserDialog();
if (nom == null || nom.isEmpty) return;
final result = await AuthApiService.createUser(nom, '1234');
if (result['success'] == true) {
final agentId = result['generated_agent_id'];
setState(() {
_agentIdController.text = agentId;
_pinController.text = '1234';
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 8),
Expanded(child: Text('Utilisateur créé: $agentId / PIN: 1234')),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${result['message']}'),
backgroundColor: Colors.orange,
),
);
}
}
Future<String?> _showCreateUserDialog() async {
final nameController = TextEditingController();
return showDialog<String>(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(Icons.person_add, color: primaryColor),
SizedBox(width: 12),
Text('Créer un utilisateur'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: primaryColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.info_outline, color: primaryColor, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
'L\'ID agent sera généré automatiquement (WRT####)',
style: TextStyle(fontSize: 12, color: primaryColor),
),
),
],
),
),
SizedBox(height: 16),
TextField(
controller: nameController,
decoration: InputDecoration(
labelText: 'Nom complet',
hintText: 'Ex: Jean Dupont',
prefixIcon: Icon(Icons.person_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
autofocus: true,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Annuler'),
),
ElevatedButton(
onPressed:
() => Navigator.pop(context, nameController.text.trim()),
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text('Créer', style: TextStyle(color: Colors.white)),
),
],
),
);
}
@override
void dispose() {
_agentIdController.dispose();
_pinController.dispose();
_animationController.dispose();
super.dispose();
}
void _login() async {
if (_formKey.currentState!.validate()) {
final authController = Provider.of<AuthController>(
context,
listen: false,
);
authController.clearError();
bool success = await authController.login(
_agentIdController.text.trim(),
_pinController.text.trim(),
false,
);
if (success && mounted) {
// Afficher un message de bienvenue personnalisé
final isEnterprise = authController.isEnterpriseMember;
final accountType = isEnterprise ? 'Entreprise' : 'Agent';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
isEnterprise ? Icons.business : Icons.person,
color: Colors.white,
size: 20,
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Bienvenue ${authController.agentName}',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
if (isEnterprise &&
authController.enterprise?.nomEntreprise != null) ...[
SizedBox(height: 2),
Text(
authController.enterprise!.nomEntreprise,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
),
),
],
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
accountType,
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
],
),
backgroundColor: isEnterprise ? enterpriseColor : Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
duration: Duration(seconds: 2),
margin: EdgeInsets.all(16),
),
);
// Navigation basée sur le rôle
final userRole = authController.role ?? 'agent';
RoleNavigator.navigateByRole(context, userRole);
} else if (mounted) {
final errorMessage =
authController.errorMessage ?? 'Erreur de connexion';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.error_outline, color: Colors.white, size: 24),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Connexion échouée',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 2),
Text(errorMessage, style: TextStyle(fontSize: 12)),
],
),
),
],
),
backgroundColor: errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
duration: Duration(seconds: 4),
margin: EdgeInsets.all(16),
),
);
}
}
}
void _openWifiSettings() async {
try {
const platform = MethodChannel('com.wortis.agent/settings');
await platform.invokeMethod('exitKioskMode');
await platform.invokeMethod('openWifiSettings');
} catch (e) {
print('Erreur ouverture WiFi: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Impossible d\'ouvrir les paramètres WiFi'),
backgroundColor: Colors.orange,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: SafeArea(
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(24),
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
children: [
SizedBox(height: 20),
_buildApiStatusIndicator(),
SizedBox(height: 20),
_buildHeader(),
SizedBox(height: 50),
_buildLoginForm(),
SizedBox(height: 30),
_buildHelpSection(),
SizedBox(height: 20),
],
),
),
),
),
),
);
}
Widget _buildApiStatusIndicator() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color:
_testingConnection
? Colors.orange.withOpacity(0.1)
: _apiConnected
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
border: Border.all(
color:
_testingConnection
? Colors.orange.withOpacity(0.3)
: _apiConnected
? Colors.green.withOpacity(0.3)
: Colors.red.withOpacity(0.3),
width: 1.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_testingConnection) ...[
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.orange),
),
),
SizedBox(width: 8),
Text(
'Test connexion...',
style: TextStyle(
color: Colors.orange.shade700,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
] else ...[
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color:
_apiConnected
? Colors.green.withOpacity(0.2)
: Colors.red.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
_apiConnected
? Icons.cloud_done_rounded
: Icons.cloud_off_rounded,
size: 16,
color:
_apiConnected ? Colors.green.shade700 : Colors.red.shade700,
),
),
SizedBox(width: 8),
Text(
_apiConnected ? 'API connectée' : 'API déconnectée',
style: TextStyle(
color:
_apiConnected ? Colors.green.shade700 : Colors.red.shade700,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 8),
GestureDetector(
onTap: _testApiConnection,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
shape: BoxShape.circle,
),
child: Icon(
Icons.refresh_rounded,
size: 16,
color:
_apiConnected
? Colors.green.shade700
: Colors.red.shade700,
),
),
),
],
],
),
);
}
Widget _buildHeader() {
return Column(
children: [
WortisLogoWidget(size: 100, isWhite: false, withShadow: true),
SizedBox(height: 24),
ShaderMask(
shaderCallback:
(bounds) => LinearGradient(
colors: [primaryColor, secondaryColor],
).createShader(bounds),
child: Text(
'WORTIS AGENT',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
),
SizedBox(height: 8),
Text(
'Connexion à votre espace',
style: TextStyle(
fontSize: 16,
color: textSecondaryColor,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 12),
// Badges des types de comptes supportés
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildAccountTypeBadge(
icon: Icons.person,
label: 'Agent',
color: primaryColor,
),
SizedBox(width: 12),
_buildAccountTypeBadge(
icon: Icons.business,
label: 'Entreprise',
color: enterpriseColor,
),
],
),
],
);
}
Widget _buildAccountTypeBadge({
required IconData icon,
required String label,
required Color color,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.3), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 14),
SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildLoginForm() {
return Container(
padding: EdgeInsets.all(28),
decoration: BoxDecoration(
color: surfaceColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 20,
offset: Offset(0, 4),
),
],
),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.login_rounded,
color: primaryColor,
size: 24,
),
),
SizedBox(width: 12),
Text(
'Identifiants',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: textPrimaryColor,
),
),
],
),
SizedBox(height: 24),
_buildTextField(
controller: _agentIdController,
label: 'ID Agent',
hint: 'Entrez votre identifiant',
prefixIcon: Icons.badge_outlined,
validator: (value) {
if (value == null || value.isEmpty) {
return 'L\'ID agent est requis';
}
if (value.length < 3) {
return 'ID agent trop court (min. 3 caractères)';
}
return null;
},
),
SizedBox(height: 20),
_buildTextField(
controller: _pinController,
label: 'Code PIN',
hint: 'Entrez votre code PIN',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePin,
suffixIcon: IconButton(
icon: Icon(
_obscurePin
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: textSecondaryColor,
),
onPressed: () {
setState(() {
_obscurePin = !_obscurePin;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Le code PIN est requis';
}
if (value.length < 4) {
return 'Le PIN doit contenir au moins 4 chiffres';
}
return null;
},
),
SizedBox(height: 32),
Consumer<AuthController>(
builder: (context, authController, child) {
return _buildLoginButton(authController);
},
),
SizedBox(height: 16),
Container(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _openWifiSettings,
icon: Icon(Icons.wifi_find_rounded, size: 20),
label: Text('Paramètres WiFi'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 14),
side: BorderSide(color: primaryColor, width: 1.5),
foregroundColor: primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
// Bouton de test (commenté par défaut)
// if (_apiConnected) ...[
// SizedBox(height: 16),
// TextButton.icon(
// onPressed: _createTestUser,
// icon: Icon(Icons.add_circle_outline, size: 18),
// label: Text('Créer utilisateur test'),
// style: TextButton.styleFrom(foregroundColor: primaryColor),
// ),
// ],
],
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required String hint,
required IconData prefixIcon,
Widget? suffixIcon,
bool obscureText = false,
String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textPrimaryColor,
),
),
SizedBox(height: 8),
TextFormField(
controller: controller,
validator: validator,
obscureText: obscureText,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(color: textSecondaryColor),
prefixIcon: Icon(prefixIcon, color: primaryColor),
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: borderColor),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: borderColor),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: primaryColor, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: errorColor),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: errorColor, width: 2),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
filled: true,
fillColor: backgroundColor.withOpacity(0.3),
),
),
],
);
}
Widget _buildLoginButton(AuthController authController) {
return Container(
height: 54,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [primaryColor, secondaryColor],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.4),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: authController.isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child:
authController.isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.login_rounded, size: 22),
SizedBox(width: 10),
Text(
'SE CONNECTER',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
],
),
),
);
}
Widget _buildHelpSection() {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
secondaryColor.withOpacity(0.1),
primaryColor.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: secondaryColor.withOpacity(0.3), width: 1.5),
),
child: Column(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.2),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Icon(
Icons.help_outline_rounded,
color: primaryColor,
size: 32,
),
),
SizedBox(height: 16),
Text(
'Besoin d\'aide ?',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: textPrimaryColor,
),
),
SizedBox(height: 12),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.7),
borderRadius: BorderRadius.circular(12),
),
// child: Column(
// children: [
// Row(
// children: [
// Icon(Icons.person, color: primaryColor, size: 16),
// SizedBox(width: 8),
// Expanded(
// child: Text(
// 'Compte Agent - Accès individuel',
// style: TextStyle(fontSize: 13, color: textPrimaryColor),
// ),
// ),
// ],
// ),
// SizedBox(height: 8),
// Row(
// children: [
// Icon(Icons.business, color: enterpriseColor, size: 16),
// SizedBox(width: 8),
// Expanded(
// child: Text(
// 'Compte Entreprise - Solde partagé',
// style: TextStyle(fontSize: 13, color: textPrimaryColor),
// ),
// ),
// ],
// ),
// ],
// ),
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: primaryColor,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.phone, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'Support: 50 05',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

1
lib/pages/profile.dart Normal file
View File

@@ -0,0 +1 @@
// TODO Implement this library.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
// lib/utils/role_navigator.dart
import 'package:flutter/material.dart';
import '../pages/main_navigation.dart';
import '../pages/admin_dashboard.dart';
import '../pages/rechargeur_dashboard.dart';
class RoleNavigator {
/// Naviguer vers la page appropriée selon le rôle
static void navigateByRole(BuildContext context, String role) {
Widget destination;
switch (role.toLowerCase()) {
case 'admin':
destination = AdminDashboard();
break;
case 'rechargeur':
destination = RechargeurDashboard();
break;
case 'agent':
default:
destination = MainNavigationPage(initialIndex: 0);
break;
}
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => destination,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: Offset(0.1, 0),
end: Offset.zero,
).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOut),
),
child: child,
),
);
},
transitionDuration: Duration(milliseconds: 600),
),
);
}
/// Obtenir l'icône selon le rôle
static IconData getRoleIcon(String role) {
switch (role.toLowerCase()) {
case 'admin':
return Icons.admin_panel_settings;
case 'rechargeur':
return Icons.add_circle;
case 'agent':
default:
return Icons.person;
}
}
/// Obtenir la couleur selon le rôle
static Color getRoleColor(String role) {
switch (role.toLowerCase()) {
case 'admin':
return Color(0xFFE53E3E); // Rouge
case 'rechargeur':
return Color(0xFF38A169); // Vert
case 'agent':
default:
return Color(0xFF006699); // Bleu Wortis
}
}
/// Obtenir le nom d'affichage du rôle
static String getRoleDisplayName(String role) {
switch (role.toLowerCase()) {
case 'admin':
return 'Administrateur';
case 'rechargeur':
return 'Rechargeur';
case 'agent':
default:
return 'Agent';
}
}
}

371
lib/pages/service.json Normal file
View File

@@ -0,0 +1,371 @@
{
"_id": {
"$oid": "67800eeeaff834f203a70cad"
},
"name": "Cartes Play Store",
"Type_Service": "PaiementImmediat",
"SecteurActivite": "Cartes Virtuelles",
"icon": "credit_score_outlined",
"banner": "https://apigede.wortispay.cg/apk-mobile/banner_service/wsgaming.jpg",
"description": "Achetez des cartes Google Play pour accéder à des applications, des jeux, de la musique et bien plus encore sur votre appareil Android.\nVeuillez vous rassurer que votre compte est bien localisé en France.",
"tite_description": "Potentiel déverrouillé",
"status": true,
"error_400": "Pas encore defenit",
"error_500": "Quelque chose a mal fonctioné",
"comment_payer": "1.\tSélectionnez le montant de la carte Play Store.\n\n2.\tRenseignez vos détails pour l'achat.\n\n3.\tPayez via Mobile Money ou carte bancaire.",
"button_service": "Payer",
"bouton_list": "Ajouter un élément",
"steps": [
{
"request": "POST",
"fields": [
{
"name": "Service",
"type": "selecteur",
"options": [
{
"value": "Google Play",
"label": "Google Play"
},
{
"value": "Apple",
"label": "Apple"
}
],
"required": true
}
],
"link": "https://api.live.wortis.cg/verify_card_apk",
"title_button": "Suivant",
"body": {
"service": "Service"
}
},
{
"request": "POST",
"fields": [
{
"name": "typeCard",
"type": "selecteur",
"label": "Choisir votre forfait",
"options": [
{
"value": "Google Play Essentiel 10 €/8 913 Fcfa",
"label": "Google Play Essentiel\n10 €/8 913 Fcfa\n"
},
{
"value": "Google Play Découverte 15 €/13 369 Fcfa",
"label": "Google Play Découverte\n15 €/13 369 Fcfa\n"
},
{
"value": "Google Avancée 25 €/22 281 Fcfa",
"label": "Google Avancée\n25 €/22 281 Fcfa\n"
},
{
"value": "Google Deluxe 50€ 50 €/44 563 Fcfa",
"label": "Google Deluxe 50€\n50 €/44 563 Fcfa\n"
},
{
"value": "Google Deluxe 100€ 100 €/89 125 Fcfa",
"label": "Google Deluxe 100€\n100 €/89 125 Fcfa"
}
],
"dependencies": [
{
"field": "service_selected",
"value": "Google Play"
}
],
"required": true
},
{
"name": "typeCard",
"type": "selecteur",
"label": "Choisir votre forfait",
"options": [
{
"value": "Apple Essentiel 10 €/8 913 Fcfa",
"label": "Apple Essentiel\n 10 €/8 913 Fcfa\n"
},
{
"value": "Apple Découverte 15 €/13 369 Fcfa",
"label": "Apple Découverte \n15 €/13 369 Fcfa\n"
},
{
"value": "Apple Avancée 25 €/22 281 Fcfa",
"label": "Apple Avancée \n25 €/22 281 Fcfa\n"
},
{
"value": "Apple Deluxe 50 €/44 563 Fcfa",
"label": "Apple Deluxe \n50 €/44 563 Fcfa"
}
],
"dependencies": [
{
"field": "service_selected",
"value": "Apple"
}
],
"required": true
},
{
"name": "nom",
"type": "text",
"label": "Nom et Prénoms",
"required": true
},
{
"name": "email",
"type": "text",
"label": "Email",
"required": true
},
{
"name": "mobile",
"type": "number",
"label": "Numéro de paiement",
"required": true
},
{
"name": "token",
"type": "hidden",
"required": true
}
],
"body": {
"nom": "nom",
"service": "service_selected",
"email": "email",
"mobile": "mobile",
"typeCard": "typeCard",
"token": "token"
},
"link_momo": "https://api.live.wortis.cg/commande_card_apk",
"api_fields": {
"service_selected": {
"type": "text",
"readonly": true,
"label": "Service Sélectionné",
"key": "service"
}
},
"link_cb": "https://api.live.wortis.cg/commande_card_apk"
}
],
"logo": "https://apigede.wortispay.cg/apk-mobile/service_logo/game.png"
}
{
"_id": {
"$oid": "677d5e75c11a9f6c491f288e"
},
"name": "Cartes Gaming",
"Type_Service": "PaiementImmediat",
"SecteurActivite": "Cartes Virtuelles",
"icon": "credit_score_outlined",
"banner": "https://apigede.wortispay.cg/apk-mobile/banner_service/wsgaming.jpg",
"description": "Achetez des cartes de jeu en ligne pour vos plateformes favorites (PlayStation, Xbox, Steam, etc.). Profitez d'une expérience de jeu ininterrompue et d'un accès rapide à des contenus premium.",
"tite_description": "Accès jeux",
"status": true,
"error_400": "Pas encore defenit",
"error_500": "Quelque chose a mal fonctioné",
"comment_payer": "1.\tSélectionnez la carte de jeu souhaitée.\n\n2.\tIndiquez le montant et vos détails de livraison.\n\n3.\tProcédez au paiement via Mobile Money ou carte bancaire.",
"button_service": "Payer",
"bouton_list": "Ajouter un élément",
"steps": [
{
"request": "POST",
"fields": [
{
"name": "Service",
"type": "selecteur",
"options": [
{
"value": "Steam",
"label": "Steam"
},
{
"value": "Nitendo",
"label": "Nitendo"
},
{
"value": "Fortnite",
"label": "Fortnite"
},
{
"value": "PlayStation",
"label": "PlayStation"
}
],
"required": true
}
],
"link": "https://api.live.wortis.cg/verify_card_apk",
"title_button": "Suivant",
"body": {
"service": "Service"
}
},
{
"request": "POST",
"fields": [
{
"name": "TypeCard",
"type": "selecteur",
"options": [
{
"value": "Steam Essentiel 10 €/ 8 913 Fcfa",
"label": "Steam Essentiel \n10 €/ 8 913 Fcfa\n"
},
{
"value": "Steam Standard 20 €/17 825 Fcfa",
"label": "Steam Standard \n20 €/17 825 Fcfa\n"
},
{
"value": "Steam Delux 50 €/ 44 563 Fcfa",
"label": "Steam Delux \n50 €/ 44 563 Fcfa\n"
},
{
"value": "Steam Ultime 100 €/ 89 125 Fcfa",
"label": "Steam Ultime \n100 €/ 89 125 Fcfa"
}
],
"dependencies": [
{
"field": "service_selected",
"value": "Steam"
}
],
"required": true
},
{
"name": "TypeCard",
"type": "selecteur",
"options": [
{
"value": "Nitendo Décourte 15 €/ 13 369 Fcfa",
"label": "Nitendo Décourte \n15 €/ 13 369 Fcfa"
},
{
"value": "Nitendo Avancée 25 €/22 281 Fcfa",
"label": "Nitendo Avancée \n25 €/22 281 Fcfa\n"
},
{
"value": "Nitendo Delux 50 €/ 44 563 Fcfa",
"label": "Nitendo Delux \n50 €/ 44 563 Fcfa\n"
},
{
"value": "Nitendo Ultime 100 €/ 89 125 Fcfa",
"label": "Nitendo Ultime \n100 €/ 89 125 Fcfa"
}
],
"dependencies": [
{
"field": "service_selected",
"value": "Nitendo"
}
],
"required": true
},
{
"name": "TypeCard",
"type": "selecteur",
"options": [
{
"value": "Fortnite-1 8.99 €/8 012 Fcfa",
"label": "Fortnite-1 \n8.99 €/8 012 Fcfa\n"
},
{
"value": "Fortnite-2 22.99 €/20 490 Fcfa",
"label": "Fortnite-2 \n22.99 €/20 490 Fcfa\n"
},
{
"value": "Fortnite-5 36.99 €/32 967 Fcfa",
"label": "Fortnite-5 \n36.99 €/32 967 Fcfa\n"
},
{
"value": "Fortnite-13 89.99 €/80 204 Fcfa",
"label": "Fortnite-13 \n89.99 €/80 204 Fcfa"
}
],
"dependencies": [
{
"field": "service_selected",
"value": "Fortnite"
}
],
"required": true
},
{
"name": "TypeCard",
"type": "selecteur",
"options": [
{
"value": "PS Essentiel 10 €/8 913 Fcfa",
"label": "PS Essentiel \n10 €/8 913 Fcfa\n"
},
{
"value": "PS Standard 20 €/17 825 Fcfa",
"label": "PS Standard \n20 €/17 825 Fcfa\n"
},
{
"value": "PS Prenium 50 €/44 463 Fcfa",
"label": "PS Prenium \n50 €/44 463 Fcfa\n"
},
{
"value": "PS Avant-Garde 100 €/89 125 Fcfa",
"label": "PS Avant-Garde \n100 €/89 125 Fcfa"
}
],
"dependencies": [
{
"field": "service_selected",
"value": "PlayStation"
}
],
"required": true
},
{
"name": "Nom et Prénoms",
"type": "text",
"required": true
},
{
"name": "Email",
"type": "text",
"required": true
},
{
"name": "Numéro de paiement",
"type": "number",
"required": true
}
],
"body": {
"nom": "Nom et Prénoms",
"service": "service_selected",
"email": "Email",
"mobile": "Numéro de paiement",
"typeCard": "typeCard"
},
"link_momo": "https://api.live.wortis.cg/commande_card_apk",
"api_fields": {
"service_selected": {
"type": "text",
"readonly": true,
"label": "Service Sélectionné",
"key": "service"
}
},
"link_cb": "https://api.live.wortis.cg/commande_card_apk"
}
],
"logo": "https://apigede.wortispay.cg/apk-mobile/service_logo/game.png"
}

View File

@@ -0,0 +1,157 @@
import 'package:shared_preferences/shared_preferences.dart';
class SessionManager {
static const String _keyUserId = 'user_id';
static const String _keyToken = 'user_token';
static const String _keyRole = 'user_role';
static const String _keyPIN = 'user_pin'; // Ajouté pour le PIN
static const String _keyLoginDate = 'login_date';
static const String _keyIsLoggedIn = 'is_logged_in';
static SessionManager? _instance;
static SharedPreferences? _prefs;
SessionManager._internal();
factory SessionManager() {
_instance ??= SessionManager._internal();
return _instance!;
}
Future<void> _initPrefs() async {
_prefs ??= await SharedPreferences.getInstance();
}
/// Sauvegarder une session
Future<void> saveSession(
String userId, {
String? token,
String? role,
String? pinkey, // Ajouté pour le PIN
}) async {
await _initPrefs();
await _prefs!.setString(_keyUserId, userId);
await _prefs!.setBool(_keyIsLoggedIn, true);
await _prefs!.setString(_keyLoginDate, DateTime.now().toIso8601String());
if (token != null) {
await _prefs!.setString(_keyToken, token);
}
if (role != null) {
await _prefs!.setString(_keyRole, role);
}
if (pinkey != null) {
await _prefs!.setString(_keyPIN, pinkey);
}
print('Session sauvegardée pour $userId');
}
/// Vérifier si l'utilisateur est connecté
Future<bool> isLoggedIn() async {
await _initPrefs();
return _prefs!.getBool(_keyIsLoggedIn) ?? false;
}
/// Récupérer l'ID utilisateur
Future<String?> getUserId() async {
await _initPrefs();
return _prefs!.getString(_keyUserId);
}
/// Récupérer le token
Future<String?> getToken() async {
await _initPrefs();
return _prefs!.getString(_keyToken);
}
/// Récupérer le rôle
Future<String?> getRole() async {
await _initPrefs();
return _prefs!.getString(_keyRole);
}
/// Récupérer le PIN (nouvelle méthode)
Future<String?> getPIN() async {
await _initPrefs();
return _prefs!.getString(_keyPIN);
}
/// Méthode synchrone pour le PIN (utilisée dans AuthController)
String? getPINSync() {
return _prefs?.getString(_keyPIN);
}
/// Récupérer la date de connexion
Future<DateTime?> getLoginDate() async {
await _initPrefs();
String? dateString = _prefs!.getString(_keyLoginDate);
if (dateString != null) {
return DateTime.parse(dateString);
}
return null;
}
/// Effacer la session
Future<void> clearSession() async {
await _initPrefs();
await _prefs!.remove(_keyUserId);
await _prefs!.remove(_keyToken);
await _prefs!.remove(_keyRole);
await _prefs!.remove(_keyPIN); // Supprimer aussi le PIN
await _prefs!.remove(_keyLoginDate);
await _prefs!.setBool(_keyIsLoggedIn, false);
print('Session effacée');
}
/// Mettre à jour le token uniquement
Future<void> updateToken(String newToken) async {
await _initPrefs();
await _prefs!.setString(_keyToken, newToken);
}
/// Vérifier si la session a expiré
Future<bool> isSessionExpired({Duration? maxAge}) async {
maxAge ??= Duration(days: 30); // 30 jours par défaut
DateTime? loginDate = await getLoginDate();
if (loginDate == null) return true;
return DateTime.now().difference(loginDate) > maxAge;
}
/// Obtenir toutes les informations de session
Future<Map<String, dynamic>> getSessionInfo() async {
await _initPrefs();
return {
'user_id': await getUserId(),
'token': await getToken(),
'role': await getRole(),
'pin': await getPIN(),
'login_date': await getLoginDate(),
'is_logged_in': await isLoggedIn(),
'is_expired': await isSessionExpired(),
};
}
/// Debug: Afficher les informations de session
Future<void> debugPrintSession() async {
Map<String, dynamic> sessionInfo = await getSessionInfo();
print('=== Informations de Session ===');
sessionInfo.forEach((key, value) {
if (key == 'pin' || key == 'token') {
// Masquer les données sensibles
print('$key: ${value?.toString().substring(0, 4) ?? 'null'}...');
} else {
print('$key: $value');
}
});
print('================================');
}
}

View File

@@ -0,0 +1,533 @@
// ===== lib/pages/splash_screen.dart MODIFIÉ AVEC API =====
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../main.dart'; // Pour AuthController
import '../widgets/wortis_logo.dart'; // NOUVEAU: Import du widget logo
import 'login.dart';
import 'main_navigation.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with TickerProviderStateMixin {
late AnimationController _logoController;
late AnimationController _textController;
late AnimationController _loadingController;
late Animation<double> _logoFadeAnimation;
late Animation<double> _logoScaleAnimation;
late Animation<Offset> _logoSlideAnimation;
late Animation<double> _textFadeAnimation;
late Animation<Offset> _textSlideAnimation;
late Animation<double> _loadingFadeAnimation;
String _statusMessage = 'Initialisation...';
bool _hasApiConnection = false;
// Couleurs Wortis
static const Color primaryColor = Color(0xFF006699);
static const Color secondaryColor = Color(0xFF0088CC);
static const Color accentColor = Color(0xFFFF6B35);
@override
void initState() {
super.initState();
_setupAnimations();
_startAnimationSequence();
_checkAuthStatus();
}
void _setupAnimations() {
_logoController = AnimationController(
duration: Duration(milliseconds: 2000),
vsync: this,
);
_textController = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_loadingController = AnimationController(
duration: Duration(milliseconds: 1200),
vsync: this,
);
_logoFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _logoController,
curve: Interval(0.0, 0.6, curve: Curves.easeOut),
),
);
_logoScaleAnimation = Tween<double>(begin: 0.3, end: 1.0).animate(
CurvedAnimation(
parent: _logoController,
curve: Interval(0.0, 0.8, curve: Curves.elasticOut),
),
);
_logoSlideAnimation = Tween<Offset>(
begin: Offset(0, -0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _logoController,
curve: Interval(0.2, 1.0, curve: Curves.easeOutCubic),
),
);
_textFadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _textController, curve: Curves.easeOut));
_textSlideAnimation = Tween<Offset>(
begin: Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(parent: _textController, curve: Curves.easeOut));
_loadingFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _loadingController, curve: Curves.easeIn),
);
}
void _startAnimationSequence() async {
_logoController.forward();
await Future.delayed(Duration(milliseconds: 800));
if (mounted) _textController.forward();
await Future.delayed(Duration(milliseconds: 400));
if (mounted) _loadingController.repeat();
}
void _updateStatus(String message) {
if (mounted) {
setState(() {
_statusMessage = message;
});
}
}
void _checkAuthStatus() async {
try {
// Étape 1: Test de connexion API
_updateStatus('Test de connexion API...');
await Future.delayed(Duration(milliseconds: 800));
if (!mounted) return;
final authController = Provider.of<AuthController>(
context,
listen: false,
);
// Tester la connexion à l'API
_hasApiConnection = await authController.testApiConnection();
if (_hasApiConnection) {
_updateStatus('API connectée ✓');
await Future.delayed(Duration(milliseconds: 600));
} else {
_updateStatus('Mode hors ligne');
await Future.delayed(Duration(milliseconds: 600));
}
// Étape 2: Vérification de l'authentification
_updateStatus('Vérification de session...');
await Future.delayed(Duration(milliseconds: 800));
if (!mounted) return;
bool isLoggedIn = await authController.checkLoginStatus(context);
print('L\'utilisateur est : $isLoggedIn');
_updateStatus('Finalisation...');
await Future.delayed(Duration(milliseconds: 500));
if (!mounted) return;
// Animation de sortie
await _logoController.reverse();
if (!mounted) return;
// Navigation
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return isLoggedIn
? MainNavigationPage(initialIndex: 0)
: LoginPage();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: Offset(0.0, 0.1),
end: Offset.zero,
).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOut),
),
child: child,
),
);
},
transitionDuration: Duration(milliseconds: 600),
),
);
} catch (e) {
print('Erreur lors de la vérification d\'authentification: $e');
_updateStatus('Erreur de connexion');
await Future.delayed(Duration(seconds: 2));
if (mounted) {
Navigator.of(
context,
).pushReplacement(MaterialPageRoute(builder: (context) => LoginPage()));
}
}
}
void _showConnectionError() {
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Text('Connexion API'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Impossible de se connecter au serveur Flask.\n',
style: TextStyle(fontWeight: FontWeight.w500),
),
Text('Vérifiez que :'),
SizedBox(height: 8),
Text('• Le serveur Flask est démarré'),
Text('• MongoDB fonctionne'),
Text('• L\'URL API est correcte'),
Text('• Votre connexion réseau'),
SizedBox(height: 12),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Vous pouvez continuer en mode hors ligne avec des données de démonstration.',
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade700,
),
),
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
_checkAuthStatus(); // Réessayer
},
child: Text('Réessayer'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => LoginPage()),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
),
child: Text(
'Continuer',
style: TextStyle(color: Colors.white),
),
),
],
),
);
}
}
@override
void dispose() {
_logoController.dispose();
_textController.dispose();
_loadingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor,
primaryColor.withOpacity(0.8),
secondaryColor,
],
stops: [0.0, 0.6, 1.0],
),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(flex: 2),
// Logo principal animé
_buildAnimatedLogo(),
SizedBox(height: 40),
// Texte principal animé
_buildAnimatedText(),
Spacer(flex: 2),
// Section loading avec statut API
_buildLoadingSection(),
SizedBox(height: 60),
],
),
),
),
),
);
}
Widget _buildAnimatedLogo() {
return AnimatedBuilder(
animation: _logoController,
builder: (context, child) {
return SlideTransition(
position: _logoSlideAnimation,
child: FadeTransition(
opacity: _logoFadeAnimation,
child: Transform.scale(
scale: _logoScaleAnimation.value,
child: WortisLogoWidget(
size: 130,
isWhite: true, // Logo blanc pour le fond bleu
withShadow: true,
),
),
),
);
},
);
}
Widget _buildAnimatedText() {
return AnimatedBuilder(
animation: _textController,
builder: (context, child) {
return SlideTransition(
position: _textSlideAnimation,
child: FadeTransition(
opacity: _textFadeAnimation,
child: Column(
children: [
Text(
'WORTIS',
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 8,
shadows: [
Shadow(
offset: Offset(0, 3),
blurRadius: 6,
color: Colors.black.withOpacity(0.4),
),
],
),
),
SizedBox(height: 12),
Text(
'Réseau Commercial',
style: TextStyle(
fontSize: 18,
color: Colors.white.withOpacity(0.9),
letterSpacing: 3,
fontWeight: FontWeight.w300,
shadows: [
Shadow(
offset: Offset(0, 1),
blurRadius: 3,
color: Colors.black.withOpacity(0.3),
),
],
),
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_hasApiConnection ? Icons.cloud_done : Icons.cloud_off,
color: Colors.white.withOpacity(0.8),
size: 14,
),
SizedBox(width: 6),
Text(
'Agent Mobile App v1.0',
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(0.8),
letterSpacing: 1,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
),
);
},
);
}
Widget _buildLoadingSection() {
return AnimatedBuilder(
animation: _loadingController,
builder: (context, child) {
return FadeTransition(
opacity: _loadingFadeAnimation,
child: Column(
children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 50,
height: 50,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white.withOpacity(0.8),
),
backgroundColor: Colors.white.withOpacity(0.2),
),
),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: _hasApiConnection ? Colors.green : Colors.orange,
shape: BoxShape.circle,
),
),
],
),
SizedBox(height: 20),
// Message de statut dynamique
Text(
_statusMessage,
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w300,
letterSpacing: 1,
),
),
SizedBox(height: 8),
Text(
'Initialisation de votre espace agent',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.7),
fontWeight: FontWeight.w300,
),
),
if (!_hasApiConnection &&
_statusMessage.contains('hors ligne')) ...[
SizedBox(height: 12),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.orange.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.wifi_off,
color: Colors.white.withOpacity(0.8),
size: 14,
),
SizedBox(width: 6),
Text(
'Mode démo',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
],
],
),
);
},
);
}
}

View File

@@ -0,0 +1,600 @@
// lib/services/connectivity_service.dart - VERSION AVEC SUPPORT ENTREPRISE
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../main.dart'; // Pour accéder à AuthController
class ConnectivityService extends ChangeNotifier {
static final ConnectivityService _instance = ConnectivityService._internal();
factory ConnectivityService() => _instance;
ConnectivityService._internal();
bool _isConnected = true;
bool _isChecking = false;
Timer? _periodicTimer;
bool _hasNetworkInterface = true;
DateTime? _lastDisconnectionTime;
int _reconnectionAttempts = 0;
bool get isConnected => _isConnected;
bool get hasNetworkInterface => _hasNetworkInterface;
DateTime? get lastDisconnectionTime => _lastDisconnectionTime;
int get reconnectionAttempts => _reconnectionAttempts;
// Clé globale pour accéder au navigator
static GlobalKey<NavigatorState>? navigatorKey;
/// Initialiser le service avec la clé navigator
static void setNavigatorKey(GlobalKey<NavigatorState> key) {
navigatorKey = key;
print('✅ NavigatorKey configuré pour ConnectivityService');
}
/// Démarrer la surveillance
void startMonitoring() {
print('🌐 Démarrage de la surveillance de connectivité');
_periodicTimer?.cancel();
_periodicTimer = Timer.periodic(Duration(seconds: 5), (timer) {
checkConnectivity();
});
// Vérification initiale après un délai
Future.delayed(Duration(seconds: 2), () {
checkConnectivity();
});
}
/// Vérifier la connectivité
Future<void> checkConnectivity() async {
if (_isChecking) return;
_isChecking = true;
try {
print('🔍 Vérification de la connectivité...');
// Étape 1: Vérifier les interfaces réseau
_hasNetworkInterface = await _checkNetworkInterfaces();
if (!_hasNetworkInterface) {
print('❌ Aucune interface réseau trouvée');
_updateConnectivityStatus(false);
return;
}
// Étape 2: Tester la connectivité internet
bool hasInternetAccess = await _testInternetConnectivity();
print('🌍 Accès internet: $hasInternetAccess');
_updateConnectivityStatus(hasInternetAccess);
} catch (e) {
print('❌ Erreur lors de la vérification: $e');
_updateConnectivityStatus(false);
} finally {
_isChecking = false;
}
}
/// Vérifier les interfaces réseau
Future<bool> _checkNetworkInterfaces() async {
try {
final interfaces = await NetworkInterface.list(
includeLinkLocal: false,
type: InternetAddressType.any,
);
bool hasActiveInterface = interfaces.any(
(interface) =>
!interface.name.toLowerCase().contains('lo') &&
interface.addresses.isNotEmpty,
);
print('📡 Interfaces réseau actives: $hasActiveInterface');
if (hasActiveInterface) {
print(
' Interfaces trouvées: ${interfaces.map((i) => i.name).join(", ")}',
);
}
return hasActiveInterface;
} catch (e) {
print('❌ Erreur vérification interfaces: $e');
return false;
}
}
/// Tester la connectivité internet avec plusieurs cibles
Future<bool> _testInternetConnectivity() async {
final testTargets = [
{'host': '1.1.1.1', 'port': 53, 'name': 'Cloudflare DNS'},
{'host': '8.8.8.8', 'port': 53, 'name': 'Google DNS'},
{'host': 'google.com', 'port': 80, 'name': 'Google'},
];
for (var target in testTargets) {
try {
final socket = await Socket.connect(
target['host'] as String,
target['port'] as int,
timeout: Duration(seconds: 3),
);
socket.destroy();
print('✅ Connexion réussie à ${target['name']}');
return true;
} catch (e) {
print('⚠️ Échec connexion à ${target['name']}: $e');
}
}
print('❌ Toutes les tentatives de connexion ont échoué');
return false;
}
/// Mettre à jour le statut de connectivité
void _updateConnectivityStatus(bool isConnected) {
if (isConnected != _isConnected) {
print('🔄 Changement connectivité: $_isConnected -> $isConnected');
final previousStatus = _isConnected;
_isConnected = isConnected;
if (!_isConnected) {
_lastDisconnectionTime = DateTime.now();
_reconnectionAttempts = 0;
_showNoConnectionDialog();
} else if (previousStatus == false && _isConnected) {
// Reconnexion réussie
print('✅ Reconnexion établie');
_reconnectionAttempts = 0;
_showReconnectionSuccess();
}
notifyListeners();
}
}
/// Afficher le dialog de perte de connexion
void _showNoConnectionDialog() {
if (navigatorKey?.currentState == null) {
print('⚠️ NavigatorState non disponible');
return;
}
print('🚨 Affichage du popup de perte de connexion');
navigatorKey!.currentState!.push(
PageRouteBuilder(
opaque: false,
barrierDismissible: false,
barrierColor: Colors.black54,
pageBuilder: (BuildContext context, _, __) {
return WillPopScope(
onWillPop: () async => false,
child: Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: _buildDialogContent(context),
),
);
},
),
);
}
/// Construire le contenu du dialog de déconnexion
Widget _buildDialogContent(BuildContext context) {
return Container(
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Icône animée
TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 800),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red[400]!, Colors.red[600]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.3),
blurRadius: 20,
offset: Offset(0, 10),
),
],
),
child: Icon(
Icons.wifi_off_rounded,
color: Colors.white,
size: 40,
),
),
);
},
),
SizedBox(height: 24),
// Titre
Text(
'Problème de connexion',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF006699),
),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
// Message détaillé
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red[200]!, width: 1),
),
child: Row(
children: [
Icon(
_hasNetworkInterface
? Icons.cloud_off_rounded
: Icons.wifi_off_rounded,
color: Colors.red[700],
size: 24,
),
SizedBox(width: 12),
Expanded(
child: Text(
_getDetailedMessage(),
style: TextStyle(
fontSize: 14,
color: Colors.red[900],
height: 1.5,
),
),
),
],
),
),
SizedBox(height: 24),
// Informations utilisateur (si connecté)
Consumer<AuthController>(
builder: (context, authController, child) {
if (authController.isLoggedIn) {
final isEnterprise = authController.isEnterpriseMember;
return Container(
padding: EdgeInsets.all(12),
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color:
isEnterprise
? Color(0xFF8B5CF6).withOpacity(0.1)
: Color(0xFF006699).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
isEnterprise
? Color(0xFF8B5CF6).withOpacity(0.3)
: Color(0xFF006699).withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
isEnterprise ? Icons.business : Icons.person,
color:
isEnterprise
? Color(0xFF8B5CF6)
: Color(0xFF006699),
size: 20,
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
authController.agentName ?? 'Agent',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
if (isEnterprise &&
authController.enterprise?.nomEntreprise !=
null) ...[
SizedBox(height: 2),
Text(
authController.enterprise!.nomEntreprise,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
],
],
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color:
isEnterprise
? Color(0xFF8B5CF6)
: Color(0xFF006699),
borderRadius: BorderRadius.circular(8),
),
child: Text(
isEnterprise ? 'Entreprise' : 'Agent',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
return SizedBox.shrink();
},
),
// Boutons d'action
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {
Navigator.of(context).pop();
_reconnectionAttempts++;
checkConnectivity();
},
icon: Icon(Icons.refresh_rounded, size: 20),
label: Text('Réessayer'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: Color(0xFF006699), width: 2),
foregroundColor: Color(0xFF006699),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
_openWifiSettings();
},
icon: Icon(Icons.wifi_find_rounded, size: 20),
label: Text('WiFi'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF006699),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
// Compteur de tentatives
if (_reconnectionAttempts > 0) ...[
SizedBox(height: 12),
Text(
'Tentative ${_reconnectionAttempts + 1}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
],
),
);
}
/// Message détaillé selon le type de problème
String _getDetailedMessage() {
if (!_hasNetworkInterface) {
return 'Aucun réseau WiFi détecté. Veuillez vous connecter à un réseau WiFi pour utiliser WORTIS Agent.';
} else {
return 'Connexion internet indisponible. Vérifiez que votre réseau WiFi a accès à internet.';
}
}
/// Afficher un message de reconnexion réussie
void _showReconnectionSuccess() {
if (navigatorKey?.currentContext == null) return;
final context = navigatorKey!.currentContext!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(Icons.wifi_rounded, color: Colors.white, size: 20),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Connexion rétablie',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 2),
Text(
'Vous pouvez continuer à utiliser l\'application',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
),
),
],
),
),
],
),
backgroundColor: Colors.green[600],
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
duration: Duration(seconds: 3),
margin: EdgeInsets.all(16),
),
);
}
/// Ouvrir les paramètres WiFi
void _openWifiSettings() async {
try {
print('📱 Ouverture des paramètres WiFi');
const platform = MethodChannel('com.wortis.agent/settings');
// Sortir du mode kiosque si activé
try {
await platform.invokeMethod('exitKioskMode');
} catch (e) {
print('⚠️ Mode kiosque non actif ou erreur: $e');
}
// Ouvrir les paramètres WiFi
await platform.invokeMethod('openWifiSettings');
// Fermer le dialog
if (navigatorKey?.currentContext != null) {
Navigator.of(navigatorKey!.currentContext!).pop();
}
} catch (e) {
print('❌ Erreur ouverture paramètres WiFi: $e');
if (navigatorKey?.currentContext != null) {
ScaffoldMessenger.of(navigatorKey!.currentContext!).showSnackBar(
SnackBar(
content: Text('Impossible d\'ouvrir les paramètres WiFi'),
backgroundColor: Colors.red,
),
);
}
}
}
/// Forcer un test manuel (pour debug)
void forceCheck() {
print('🔧 Test forcé de connectivité');
checkConnectivity();
}
/// Méthode de test pour forcer l'affichage du popup
void showTestPopup() {
print('🧪 Test d\'affichage du popup de déconnexion');
_isConnected = false;
_lastDisconnectionTime = DateTime.now();
_reconnectionAttempts = 0;
_showNoConnectionDialog();
}
/// Simuler une reconnexion (pour test)
void simulateReconnection() {
print('🧪 Simulation de reconnexion');
_updateConnectivityStatus(true);
}
/// Obtenir les statistiques de connectivité
Map<String, dynamic> getConnectivityStats() {
return {
'is_connected': _isConnected,
'has_network_interface': _hasNetworkInterface,
'last_disconnection': _lastDisconnectionTime?.toIso8601String(),
'reconnection_attempts': _reconnectionAttempts,
'is_checking': _isChecking,
};
}
/// Réinitialiser les statistiques
void resetStats() {
_reconnectionAttempts = 0;
_lastDisconnectionTime = null;
print('📊 Statistiques de connectivité réinitialisées');
}
@override
void dispose() {
print('🛑 Arrêt du service de connectivité');
_periodicTimer?.cancel();
super.dispose();
}
}
/// Extension pour des méthodes utilitaires
extension ConnectivityServiceExtension on ConnectivityService {
/// Vérifier si la connexion est stable
bool get isStable {
return _isConnected && _reconnectionAttempts == 0;
}
/// Obtenir le temps depuis la dernière déconnexion
Duration? get timeSinceLastDisconnection {
if (_lastDisconnectionTime == null) return null;
return DateTime.now().difference(_lastDisconnectionTime!);
}
/// Obtenir un message de statut lisible
String get statusMessage {
if (_isConnected) {
if (_reconnectionAttempts > 0) {
return 'Connexion rétablie après $_reconnectionAttempts tentatives';
}
return 'Connecté';
} else {
if (!_hasNetworkInterface) {
return 'Aucun réseau WiFi';
}
return 'Pas de connexion internet';
}
}
}

View File

@@ -0,0 +1,561 @@
// Enhanced lib/services/wortis_api_service.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../models/service_model.dart';
class WortisApiService {
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
// Singleton pattern
static final WortisApiService _instance = WortisApiService._internal();
factory WortisApiService() => _instance;
WortisApiService._internal();
// Retry configuration
static const int maxRetries = 3;
static const Duration retryDelay = Duration(seconds: 2);
Future<List<WortisService>> getServices() async {
return _retryRequest<List<WortisService>>(() async {
final response = await http
.get(
Uri.parse('$baseUrl/get_services_back'),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Connection': 'keep-alive',
},
)
.timeout(Duration(seconds: 30));
if (response.statusCode == 200) {
final Map<String, dynamic> jsonData = json.decode(response.body);
final List<dynamic> servicesJson = jsonData['all_services'] ?? [];
return servicesJson
.map((serviceJson) => WortisService.fromJson(serviceJson))
.toList();
} else {
throw Exception('Erreur API: ${response.statusCode}');
}
});
}
// Méthode pour grouper les services par secteur
Map<String, List<WortisService>> groupServicesBySector(
List<WortisService> services,
) {
Map<String, List<WortisService>> grouped = {};
for (var service in services) {
String sector = service.secteurActivite;
if (grouped[sector] == null) {
grouped[sector] = [];
}
grouped[sector]!.add(service);
}
return grouped;
}
// Generic retry logic
Future<T> _retryRequest<T>(Future<T> Function() request) async {
Exception? lastException;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await request();
} on SocketException catch (e) {
lastException = e;
print('Tentative $attempt échouée (SocketException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} on HttpException catch (e) {
lastException = e;
print('Tentative $attempt échouée (HttpException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} on http.ClientException catch (e) {
lastException = e;
print('Tentative $attempt échouée (ClientException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} catch (e) {
// For other exceptions, don't retry
rethrow;
}
}
throw Exception(
'Impossible de se connecter après $maxRetries tentatives: $lastException',
);
}
}
class AuthApiService {
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
static const int maxRetries = 3;
static const Duration retryDelay = Duration(seconds: 2);
/// Test de connexion à l'API avec retry
static Future<bool> testConnection() async {
try {
return await _retryRequest<bool>(() async {
final response = await http
.get(
Uri.parse('http://google.com/'),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
)
.timeout(Duration(seconds: 10));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['success'] == true;
}
return false;
});
} catch (e) {
print('Erreur test connexion après plusieurs tentatives: $e');
return false;
}
}
/// Connexion utilisateur avec retry
static Future<Map<String, dynamic>> login(String agentId, String pin) async {
try {
return await _retryRequest<Map<String, dynamic>>(() async {
final response = await http
.post(
Uri.parse('$baseUrl/auth/login'),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
body: jsonEncode({'agent_id': agentId, 'pin': pin}),
)
.timeout(Duration(seconds: 15));
return jsonDecode(response.body);
});
} catch (e) {
print('Erreur login API après retry: $e');
return {'success': false, 'message': 'Erreur de connexion au serveur'};
}
}
/// Créer un utilisateur avec retry
static Future<Map<String, dynamic>> createUser(
String nom,
String pin, {
String role = 'agent',
}) async {
try {
return await _retryRequest<Map<String, dynamic>>(() async {
final response = await http
.post(
Uri.parse('$baseUrl/users/register'),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
body: jsonEncode({'nom': nom, 'pin': pin, 'role': role}),
)
.timeout(Duration(seconds: 15));
return jsonDecode(response.body);
});
} catch (e) {
print('Erreur création utilisateur après retry: $e');
return {'success': false, 'message': 'Erreur de création'};
}
}
/// Récupérer le solde d'un agent avec retry
static Future<Map<String, dynamic>> getAgentBalance(String agentId) async {
try {
return await _retryRequest<Map<String, dynamic>>(() async {
final response = await http
.get(
Uri.parse('$baseUrl/agent/$agentId/balance'),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
)
.timeout(Duration(seconds: 15));
return jsonDecode(response.body);
});
} catch (e) {
print('Erreur récupération solde après retry: $e');
return {
'success': false,
'message': 'Erreur lors de la récupération du solde',
};
}
}
/// Recharger le solde d'un agent avec retry
static Future<Map<String, dynamic>> rechargeAgent(
String agentId,
double montant,
) async {
try {
return await _retryRequest<Map<String, dynamic>>(() async {
final response = await http
.post(
Uri.parse('$baseUrl/agent/$agentId/recharge'),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
body: jsonEncode({'montant': montant}),
)
.timeout(Duration(seconds: 15));
return jsonDecode(response.body);
});
} catch (e) {
print('Erreur recharge après retry: $e');
return {'success': false, 'message': 'Erreur lors de la recharge'};
}
}
/// Mettre à jour le solde d'un agent avec retry
static Future<Map<String, dynamic>> updateAgentBalance(
String agentId,
double nouveauSolde,
) async {
try {
return await _retryRequest<Map<String, dynamic>>(() async {
final response = await http
.put(
Uri.parse('$baseUrl/agent/$agentId/balance'),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
body: jsonEncode({'solde': nouveauSolde}),
)
.timeout(Duration(seconds: 15));
return jsonDecode(response.body);
});
} catch (e) {
print('Erreur mise à jour solde après retry: $e');
return {
'success': false,
'message': 'Erreur lors de la mise à jour du solde',
};
}
}
/// Enregistrer une transaction avec retry
static Future<Map<String, dynamic>> recordTransaction({
required String agentId,
required String serviceId,
required String serviceName,
required double montant,
required double commission,
required String typeTransaction,
required Map<String, dynamic> detailsTransaction,
}) async {
try {
return await _retryRequest<Map<String, dynamic>>(() async {
final response = await http
.post(
Uri.parse('$baseUrl/transactions'),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
body: jsonEncode({
'agent_id': agentId,
'service_id': serviceId,
'service_name': serviceName,
'montant': montant,
'commission': commission,
'type_transaction': typeTransaction,
'details': detailsTransaction,
'date_transaction': DateTime.now().toIso8601String(),
}),
)
.timeout(Duration(seconds: 15));
return jsonDecode(response.body);
});
} catch (e) {
print('Erreur enregistrement transaction après retry: $e');
return {
'success': false,
'message': 'Erreur lors de l\'enregistrement de la transaction',
};
}
}
// Generic retry logic for AuthApiService
static Future<T> _retryRequest<T>(Future<T> Function() request) async {
Exception? lastException;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await request();
} on SocketException catch (e) {
lastException = e;
print('AuthAPI - Tentative $attempt échouée (SocketException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} on HttpException catch (e) {
lastException = e;
print('AuthAPI - Tentative $attempt échouée (HttpException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} on http.ClientException catch (e) {
lastException = e;
print('AuthAPI - Tentative $attempt échouée (ClientException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} catch (e) {
// For other exceptions, don't retry
rethrow;
}
}
throw Exception(
'Impossible de se connecter après $maxRetries tentatives: $lastException',
);
}
}
class ApiService {
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
static const int maxRetries = 3;
static const Duration retryDelay = Duration(seconds: 2);
/// Récupérer les champs d'un service avec retry amélioré
Future<Map<String, dynamic>> fetchServiceFields(String serviceName) async {
return _retryRequest<Map<String, dynamic>>(() async {
// Fix: Remove double slash in URL
final url = '$baseUrl/service/${Uri.encodeComponent(serviceName)}';
print('Fetching service fields from: $url');
final response = await http
.get(
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Connection': 'keep-alive',
'User-Agent': 'WortisApp/1.0',
},
)
.timeout(Duration(seconds: 30));
print('Response status: ${response.statusCode}');
print('Response headers: ${response.headers}');
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception(
'Erreur API: ${response.statusCode} - ${response.body}',
);
}
});
}
/// Vérifier les données avec GET et retry
Future<Map<String, dynamic>> verifyDataGet(
String url,
Map<String, dynamic> params,
) async {
return _retryRequest<Map<String, dynamic>>(() async {
final uri = Uri.parse(url).replace(
queryParameters: params.map(
(key, value) => MapEntry(key, value.toString()),
),
);
final response = await http
.get(
uri,
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
)
.timeout(Duration(seconds: 30));
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception(
'Données non trouvées - Status: ${response.statusCode}',
);
}
});
}
/// Vérifier les données avec POST et retry
Future<Map<String, dynamic>> verifyDataPost(
String url,
Map<String, dynamic> data,
String operationId,
) async {
return _retryRequest<Map<String, dynamic>>(() async {
final response = await http
.post(
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
body: jsonEncode(data),
)
.timeout(Duration(seconds: 30));
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception(
'Données non trouvées - Status: ${response.statusCode}',
);
}
});
}
/// Soumettre les données du formulaire avec retry
Future<void> submitFormData(
BuildContext context,
String url,
Map<String, dynamic> data,
Map<String, dynamic>? serviceData,
dynamic additionalData,
bool isCardPayment,
) async {
try {
await _retryRequest<void>(() async {
print(url);
final response = await http
.post(
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
body: jsonEncode(data),
)
.timeout(Duration(seconds: 30));
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
// Afficher un message de succès
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 8),
Text('Transaction effectuée avec succès'),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
// Retourner à la page précédente
Navigator.of(context).pop();
}
} else {
throw Exception(
'Erreur lors de la soumission - Status: ${response.statusCode}',
);
}
});
} catch (e) {
print('Erreur submitFormData après retry: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.error, color: Colors.white),
SizedBox(width: 8),
Expanded(
child: Text(
'Erreur lors de la transaction: ${e.toString()}',
maxLines: 2,
),
),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
duration: Duration(seconds: 5),
),
);
}
}
}
/// Annuler l'opération en cours
void cancelOperation() {
print('Opération annulée');
}
// Generic retry logic for ApiService
Future<T> _retryRequest<T>(Future<T> Function() request) async {
Exception? lastException;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await request();
} on SocketException catch (e) {
lastException = e;
print('API - Tentative $attempt échouée (SocketException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} on HttpException catch (e) {
lastException = e;
print('API - Tentative $attempt échouée (HttpException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} on http.ClientException catch (e) {
lastException = e;
print('API - Tentative $attempt échouée (ClientException): $e');
if (attempt < maxRetries) {
await Future.delayed(retryDelay * attempt);
}
} catch (e) {
// For other exceptions, don't retry
rethrow;
}
}
throw Exception(
'Impossible de se connecter après $maxRetries tentatives: $lastException',
);
}
}

2651
lib/tpe.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,328 @@
// lib/widgets/balance_widget.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../services/wortis_api_service.dart';
class BalanceWidget extends StatefulWidget {
final bool showIcon;
final bool compact;
final Color? textColor;
final Color? backgroundColor;
const BalanceWidget({
super.key,
this.showIcon = true,
this.compact = false,
this.textColor,
this.backgroundColor,
});
@override
_BalanceWidgetState createState() => _BalanceWidgetState();
}
class _BalanceWidgetState extends State<BalanceWidget> {
bool _isRefreshing = false;
DateTime _lastRefresh = DateTime.now();
@override
void initState() {
super.initState();
// Rafraîchir le solde au démarrage
WidgetsBinding.instance.addPostFrameCallback((_) {
_refreshBalance();
});
}
Future<void> _refreshBalance() async {
final authController = Provider.of<AuthController>(context, listen: false);
final agentId = authController.agentId;
if (agentId == null || _isRefreshing) return;
setState(() {
_isRefreshing = true;
});
try {
// Test de connexion API d'abord
final isConnected = await AuthApiService.testConnection();
if (isConnected) {
final response = await AuthApiService.getAgentBalance(agentId);
if (response['success'] == true && mounted) {
final nouveauSolde = (response['solde'] ?? 0.0).toDouble();
authController.updateBalance(nouveauSolde);
setState(() {
_lastRefresh = DateTime.now();
});
// Afficher un message si le solde a changé significativement
final ancienSolde = authController.balance;
if ((nouveauSolde - ancienSolde).abs() > 100) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.refresh, color: Colors.white, size: 16),
SizedBox(width: 8),
Text(
'Solde mis à jour: ${nouveauSolde.toStringAsFixed(0)} XAF',
),
],
),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
}
}
} catch (e) {
print('Erreur rafraîchissement solde: $e');
} finally {
if (mounted) {
setState(() {
_isRefreshing = false;
});
}
}
}
String _getTimeAgo() {
final now = DateTime.now();
final difference = now.difference(_lastRefresh);
if (difference.inMinutes < 1) {
return 'À l\'instant';
} else if (difference.inMinutes < 60) {
return 'Il y a ${difference.inMinutes}min';
} else {
return 'Il y a ${difference.inHours}h';
}
}
@override
Widget build(BuildContext context) {
return Consumer<AuthController>(
builder: (context, authController, child) {
// Utiliser le solde actif (entreprise ou personnel selon le type de compte)
final solde = authController.activeBalance;
final isEnterprise = authController.isEnterpriseMember;
final textColor = widget.textColor ?? Colors.white;
if (widget.compact) {
return GestureDetector(
onTap: _refreshBalance,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: widget.backgroundColor ?? Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: (widget.backgroundColor ?? Colors.white).withOpacity(
0.3,
),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showIcon) ...[
Icon(
isEnterprise ? Icons.business_center : Icons.account_balance_wallet,
color: textColor,
size: 14,
),
SizedBox(width: 6),
],
if (_isRefreshing)
SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1.5,
valueColor: AlwaysStoppedAnimation<Color>(textColor),
),
)
else
Text(
'${solde.toStringAsFixed(0)} XAF',
style: TextStyle(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
// Version étendue
final enterpriseColor = Color(0xFF8B5CF6);
final displayColor = isEnterprise ? enterpriseColor : Color(0xFF006699);
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: widget.backgroundColor ?? Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isEnterprise ? Icons.business_center : Icons.account_balance_wallet,
color: displayColor,
size: 24,
),
SizedBox(width: 8),
Flexible(
child: Text(
isEnterprise ? 'Solde entreprise' : 'Solde disponible',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
if (isEnterprise && authController.enterprise?.nomEntreprise != null) ...[
SizedBox(height: 4),
Text(
authController.enterprise!.nomEntreprise,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
],
],
),
),
GestureDetector(
onTap: _isRefreshing ? null : _refreshBalance,
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: displayColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child:
_isRefreshing
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(displayColor),
),
)
: Icon(
Icons.refresh,
color: displayColor,
size: 16,
),
),
),
],
),
SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Text(
solde.toStringAsFixed(0),
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: displayColor,
),
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 8),
Padding(
padding: EdgeInsets.only(bottom: 4),
child: Text(
'XAF',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
),
),
),
],
),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
'Dernière mise à jour: ${_getTimeAgo()}',
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
),
),
if (isEnterprise)
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: enterpriseColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.business, size: 10, color: enterpriseColor),
SizedBox(width: 4),
Text(
'PRO',
style: TextStyle(
fontSize: 10,
color: enterpriseColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
],
),
);
},
);
}
}

View File

View File

@@ -0,0 +1,484 @@
// widgets/pin_verification_dialog.dart - Version mise à jour
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class PinVerificationDialog extends StatefulWidget {
final String agentName;
final String serviceName;
final double baseAmount;
final double fees;
final double totalAmount;
final double agentBalance; // Nouveau paramètre
final Function(String) onPinConfirmed;
final VoidCallback onCancel;
const PinVerificationDialog({
super.key,
required this.agentName,
required this.serviceName,
required this.baseAmount,
required this.fees,
required this.totalAmount,
required this.agentBalance, // Nouveau paramètre requis
required this.onPinConfirmed,
required this.onCancel,
});
@override
_PinVerificationDialogState createState() => _PinVerificationDialogState();
}
class _PinVerificationDialogState extends State<PinVerificationDialog> {
final TextEditingController _pinController = TextEditingController();
bool _isObscured = true;
bool _isLoading = false;
String? _errorMessage;
// Formatter pour les montants
String _formatAmount(double amount) {
final formatter = NumberFormat('#,###', 'fr_FR');
return formatter.format(amount);
}
// Vérifier si l'agent a suffisamment de solde
bool get _hasSufficientBalance {
return widget.agentBalance >= widget.totalAmount;
}
@override
void dispose() {
_pinController.dispose();
super.dispose();
}
Future<void> _handlePinConfirmation() async {
if (_pinController.text.length < 4) {
setState(() {
_errorMessage = 'Le PIN doit contenir au moins 4 chiffres';
});
return;
}
if (!_hasSufficientBalance) {
setState(() {
_errorMessage = 'Solde insuffisant pour effectuer cette transaction';
});
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// Simuler une vérification (remplacez par votre logique de vérification)
await Future.delayed(const Duration(seconds: 1));
widget.onPinConfirmed(_pinController.text);
} catch (e) {
setState(() {
_errorMessage = 'Erreur lors de la vérification du PIN';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final isSmallScreen = size.width < 360;
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 0,
backgroundColor: Colors.transparent,
child: Container(
constraints: BoxConstraints(
maxWidth: size.width * 0.9,
maxHeight: size.height * 0.8,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 0,
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(isSmallScreen ? 16 : 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// En-tête avec icône de sécurité
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF006699).withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.security,
size: 32,
color: Color(0xFF006699),
),
),
const SizedBox(height: 16),
// Titre
Text(
'Confirmation sécurisée',
style: TextStyle(
fontSize: isSmallScreen ? 20 : 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Vérifiez les détails et confirmez avec votre PIN',
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
color: Colors.black54,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Informations de la transaction
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
children: [
// Service
_buildDetailRow(
icon: Icons.business_center,
label: 'Service',
value: widget.serviceName,
isSmallScreen: isSmallScreen,
),
if (widget.baseAmount > 0) ...[
const SizedBox(height: 12),
_buildDetailRow(
icon: Icons.account_balance_wallet,
label: 'Montant à payer',
value: '${_formatAmount(widget.baseAmount)} FCFA',
isSmallScreen: isSmallScreen,
),
],
if (widget.fees > 0) ...[
const SizedBox(height: 12),
_buildDetailRow(
icon: Icons.receipt_long,
label: 'Frais',
value: '${_formatAmount(widget.fees)} FCFA',
isSmallScreen: isSmallScreen,
),
],
const SizedBox(height: 12),
Divider(color: Colors.grey[300]),
const SizedBox(height: 12),
// Total
_buildDetailRow(
icon: Icons.payments,
label: 'Total',
value: '${_formatAmount(widget.totalAmount)} FCFA',
isTotal: true,
isSmallScreen: isSmallScreen,
),
const SizedBox(height: 16),
// Solde agent avec indicateur de suffisance
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color:
_hasSufficientBalance
? Colors.green[50]
: Colors.red[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
_hasSufficientBalance
? Colors.green[200]!
: Colors.red[200]!,
),
),
child: Row(
children: [
Icon(
_hasSufficientBalance
? Icons.check_circle
: Icons.warning,
color:
_hasSufficientBalance
? Colors.green[600]
: Colors.red[600],
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Votre solde',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
Text(
'${_formatAmount(widget.agentBalance)} FCFA',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color:
_hasSufficientBalance
? Colors.green[700]
: Colors.red[700],
),
),
if (!_hasSufficientBalance)
Text(
'Solde insuffisant',
style: TextStyle(
fontSize: 12,
color: Colors.red[600],
fontStyle: FontStyle.italic,
),
),
],
),
),
],
),
),
],
),
),
const SizedBox(height: 24),
// Champ PIN
Text(
'PIN de sécurité',
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
TextField(
controller: _pinController,
keyboardType: TextInputType.number,
obscureText: _isObscured,
maxLength: 6,
enabled: !_isLoading && _hasSufficientBalance,
decoration: InputDecoration(
hintText: 'Saisissez votre PIN',
prefixIcon: const Icon(
Icons.lock,
color: Color(0xFF006699),
),
suffixIcon: IconButton(
icon: Icon(
_isObscured ? Icons.visibility : Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_isObscured = !_isObscured;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE0E7FF)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE0E7FF)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFF006699),
width: 2,
),
),
counterText: '',
),
onSubmitted:
_hasSufficientBalance
? (_) => _handlePinConfirmation()
: null,
),
if (_errorMessage != null) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red[200]!),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Colors.red[600],
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
color: Colors.red[600],
fontSize: 12,
),
),
),
],
),
),
],
const SizedBox(height: 24),
// Boutons
Row(
children: [
Expanded(
child: TextButton(
onPressed: _isLoading ? null : widget.onCancel,
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(
vertical: isSmallScreen ? 12 : 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Annuler',
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
color: Colors.grey[600],
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed:
(_isLoading || !_hasSufficientBalance)
? null
: _handlePinConfirmation,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF006699),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
vertical: isSmallScreen ? 12 : 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child:
_isLoading
? SizedBox(
height: isSmallScreen ? 16 : 20,
width: isSmallScreen ? 16 : 20,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text(
'CONFIRMER',
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
],
),
),
),
),
);
}
Widget _buildDetailRow({
required IconData icon,
required String label,
required String value,
bool isTotal = false,
required bool isSmallScreen,
}) {
return Row(
children: [
Icon(
icon,
size: isSmallScreen ? 16 : 18,
color: isTotal ? const Color(0xFF006699) : Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: Text(
label,
style: TextStyle(
fontSize: isSmallScreen ? 13 : 14,
fontWeight: isTotal ? FontWeight.w600 : FontWeight.w500,
color: isTotal ? const Color(0xFF006699) : Colors.grey[700],
),
),
),
Text(
value,
style: TextStyle(
fontSize: isSmallScreen ? 13 : 14,
fontWeight: isTotal ? FontWeight.bold : FontWeight.w600,
color: isTotal ? const Color(0xFF006699) : Colors.black87,
),
),
],
);
}
}

View File

@@ -0,0 +1,377 @@
// ===== lib/utils/responsive_helper.dart =====
// Système de responsivité centralisé pour tablettes et rotation
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:math' as math;
/// Gestionnaire principal de la responsivité et rotation
class ResponsiveHelper {
// =================== CONSTANTES DE BREAKPOINTS ===================
/// Breakpoints pour différentes tailles d'écran
static const double phoneMaxWidth = 400;
static const double tabletMinWidth = 600;
static const double desktopMinWidth = 1024;
/// Hauteurs critiques pour détection d'orientation
static const double portraitMinHeight = 600;
static const double landscapeMaxHeight = 500;
// =================== DÉTECTION DE TYPE D'APPAREIL ===================
/// Détermine si c'est un petit téléphone
static bool isSmallPhone(BuildContext context) {
return MediaQuery.of(context).size.width < phoneMaxWidth;
}
/// Détermine si c'est un téléphone standard
static bool isPhone(BuildContext context) {
return MediaQuery.of(context).size.width < tabletMinWidth;
}
/// Détermine si c'est une tablette
static bool isTablet(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width >= tabletMinWidth && width < desktopMinWidth;
}
/// Détermine si c'est un grand écran/desktop
static bool isDesktop(BuildContext context) {
return MediaQuery.of(context).size.width >= desktopMinWidth;
}
// =================== DÉTECTION D'ORIENTATION ===================
/// Détection précise de l'orientation
static bool isPortrait(BuildContext context) {
final size = MediaQuery.of(context).size;
return size.height > size.width;
}
/// Détection du mode paysage
static bool isLandscape(BuildContext context) {
final size = MediaQuery.of(context).size;
return size.width > size.height;
}
/// Détection spéciale pour tablette en mode paysage
static bool isTabletLandscape(BuildContext context) {
return isTablet(context) && isLandscape(context);
}
/// Détection spéciale pour téléphone en mode paysage
static bool isPhoneLandscape(BuildContext context) {
return isPhone(context) && isLandscape(context);
}
// =================== CONFIGURATION D'ORIENTATION ===================
/// Force l'orientation portrait et paysage pour tablettes
static void enableAllOrientations() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
/// Force uniquement le mode portrait (pour certains écrans si nécessaire)
static void enablePortraitOnly() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
}
/// Force uniquement le mode paysage
static void enableLandscapeOnly() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
// =================== DIMENSIONS RESPONSIVES ===================
/// Obtient la largeur responsive selon l'appareil
static double getResponsiveWidth(BuildContext context, {
double? phone,
double? tablet,
double? desktop,
}) {
if (isDesktop(context)) return desktop ?? tablet ?? phone ?? 0;
if (isTablet(context)) return tablet ?? phone ?? 0;
return phone ?? 0;
}
/// Obtient la hauteur responsive selon l'appareil
static double getResponsiveHeight(BuildContext context, {
double? phone,
double? tablet,
double? desktop,
}) {
if (isDesktop(context)) return desktop ?? tablet ?? phone ?? 0;
if (isTablet(context)) return tablet ?? phone ?? 0;
return phone ?? 0;
}
/// Padding responsive adaptatif
static EdgeInsets getResponsivePadding(BuildContext context, {
EdgeInsets? phone,
EdgeInsets? tablet,
EdgeInsets? desktop,
}) {
if (isDesktop(context)) return desktop ?? tablet ?? phone ?? EdgeInsets.zero;
if (isTablet(context)) return tablet ?? phone ?? EdgeInsets.zero;
return phone ?? EdgeInsets.zero;
}
/// Taille de police responsive
static double getResponsiveFontSize(BuildContext context, {
double? phone,
double? tablet,
double? desktop,
}) {
if (isDesktop(context)) return desktop ?? tablet ?? phone ?? 14;
if (isTablet(context)) return tablet ?? phone ?? 14;
return phone ?? 14;
}
// =================== GRILLES RESPONSIVES ===================
/// Nombre de colonnes selon la taille d'écran
static int getGridColumns(BuildContext context, {
int phoneColumns = 2,
int tabletPortraitColumns = 3,
int tabletLandscapeColumns = 4,
int desktopColumns = 5,
}) {
if (isDesktop(context)) return desktopColumns;
if (isTabletLandscape(context)) return tabletLandscapeColumns;
if (isTablet(context)) return tabletPortraitColumns;
return phoneColumns;
}
/// Ratio d'aspect adaptatif pour les cartes
static double getCardAspectRatio(BuildContext context) {
if (isPhoneLandscape(context)) return 2.5;
if (isTabletLandscape(context)) return 1.8;
if (isTablet(context)) return 1.4;
return 1.2; // Portrait par défaut
}
// =================== WIDGETS RESPONSIVES ===================
/// Container responsive avec padding adaptatif
static Widget responsiveContainer({
required BuildContext context,
required Widget child,
EdgeInsets? phonePadding,
EdgeInsets? tabletPadding,
EdgeInsets? desktopPadding,
Color? backgroundColor,
BorderRadius? borderRadius,
}) {
return Container(
padding: getResponsivePadding(
context,
phone: phonePadding ?? const EdgeInsets.all(16),
tablet: tabletPadding ?? const EdgeInsets.all(24),
desktop: desktopPadding ?? const EdgeInsets.all(32),
),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: borderRadius ?? BorderRadius.circular(12),
),
child: child,
);
}
/// Texte responsive avec tailles adaptatives
static Widget responsiveText(
String text, {
required BuildContext context,
double? phoneSize,
double? tabletSize,
double? desktopSize,
FontWeight? fontWeight,
Color? color,
TextAlign? textAlign,
int? maxLines,
TextOverflow? overflow,
}) {
return Text(
text,
style: TextStyle(
fontSize: getResponsiveFontSize(
context,
phone: phoneSize ?? 14,
tablet: tabletSize ?? 16,
desktop: desktopSize ?? 18,
),
fontWeight: fontWeight,
color: color,
),
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
// =================== LAYOUTS RESPONSIVES ===================
/// Layout responsive en colonnes
static Widget responsiveRow({
required BuildContext context,
required List<Widget> children,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
bool forceColumnOnPhone = true,
}) {
if (forceColumnOnPhone && isPhone(context)) {
return Column(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: children,
);
}
return Row(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: children,
);
}
/// GridView responsive adaptatif
static Widget responsiveGridView({
required BuildContext context,
required List<Widget> children,
int? phoneColumns,
int? tabletPortraitColumns,
int? tabletLandscapeColumns,
int? desktopColumns,
double childAspectRatio = 1.0,
double crossAxisSpacing = 8.0,
double mainAxisSpacing = 8.0,
EdgeInsets? padding,
ScrollPhysics? physics,
}) {
final columns = getGridColumns(
context,
phoneColumns: phoneColumns ?? 2,
tabletPortraitColumns: tabletPortraitColumns ?? 3,
tabletLandscapeColumns: tabletLandscapeColumns ?? 4,
desktopColumns: desktopColumns ?? 5,
);
return GridView.count(
shrinkWrap: true,
physics: physics ?? const NeverScrollableScrollPhysics(),
crossAxisCount: columns,
childAspectRatio: childAspectRatio,
crossAxisSpacing: crossAxisSpacing,
mainAxisSpacing: mainAxisSpacing,
padding: padding ?? getResponsivePadding(
context,
phone: const EdgeInsets.all(16),
tablet: const EdgeInsets.all(24),
),
children: children,
);
}
// =================== NAVIGATION RESPONSIVE ===================
/// Détermine si on doit utiliser un Drawer ou une BottomNavigationBar
static bool shouldUseDrawer(BuildContext context) {
return isTabletLandscape(context) || isDesktop(context);
}
/// Détermine si on doit utiliser une navigation rail (côté gauche)
static bool shouldUseNavigationRail(BuildContext context) {
return isTabletLandscape(context) || isDesktop(context);
}
// =================== ORIENTATIONS SPÉCIFIQUES ===================
/// Initialise les orientations selon l'appareil
static void initializeOrientations(BuildContext context) {
if (isTablet(context) || isDesktop(context)) {
enableAllOrientations();
} else {
// Pour les téléphones, on peut choisir de limiter ou non
enableAllOrientations(); // Ou enablePortraitOnly() selon les besoins
}
}
// =================== UTILITAIRES DIVERS ===================
/// Obtient la largeur maximale recommandée pour le contenu
static double getMaxContentWidth(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
if (isDesktop(context)) {
return math.min(screenWidth * 0.8, 1200); // Max 1200px sur desktop
} else if (isTablet(context)) {
return screenWidth * 0.9; // 90% sur tablette
}
return screenWidth; // 100% sur téléphone
}
/// Centre le contenu avec une largeur maximale
static Widget centerContent({
required BuildContext context,
required Widget child,
double? maxWidth,
}) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: maxWidth ?? getMaxContentWidth(context),
),
child: child,
),
);
}
/// Détermine l'espacement entre les éléments
static double getSpacing(BuildContext context, {
double? small,
double? medium,
double? large,
}) {
if (isDesktop(context)) return large ?? 24;
if (isTablet(context)) return medium ?? 16;
return small ?? 12;
}
}
// =================== EXTENSION POUR MEDIAQUERY ===================
/// Extension pour simplifier l'utilisation de ResponsiveHelper
extension ResponsiveExtension on BuildContext {
bool get isSmallPhone => ResponsiveHelper.isSmallPhone(this);
bool get isPhone => ResponsiveHelper.isPhone(this);
bool get isTablet => ResponsiveHelper.isTablet(this);
bool get isDesktop => ResponsiveHelper.isDesktop(this);
bool get isPortrait => ResponsiveHelper.isPortrait(this);
bool get isLandscape => ResponsiveHelper.isLandscape(this);
bool get isTabletLandscape => ResponsiveHelper.isTabletLandscape(this);
bool get isPhoneLandscape => ResponsiveHelper.isPhoneLandscape(this);
double responsiveWidth({double? phone, double? tablet, double? desktop}) =>
ResponsiveHelper.getResponsiveWidth(this, phone: phone, tablet: tablet, desktop: desktop);
double responsiveHeight({double? phone, double? tablet, double? desktop}) =>
ResponsiveHelper.getResponsiveHeight(this, phone: phone, tablet: tablet, desktop: desktop);
double responsiveFontSize({double? phone, double? tablet, double? desktop}) =>
ResponsiveHelper.getResponsiveFontSize(this, phone: phone, tablet: tablet, desktop: desktop);
EdgeInsets responsivePadding({EdgeInsets? phone, EdgeInsets? tablet, EdgeInsets? desktop}) =>
ResponsiveHelper.getResponsivePadding(this, phone: phone, tablet: tablet, desktop: desktop);
}

View File

@@ -0,0 +1,108 @@
// ===== lib/widgets/wortis_logo.dart FONCTIONNEL =====
import 'package:flutter/material.dart';
class WortisLogoWidget extends StatelessWidget {
final double size;
final bool isWhite;
final bool withShadow;
final String? customPath;
const WortisLogoWidget({
super.key,
this.size = 100,
this.isWhite = false,
this.withShadow = true,
this.customPath,
});
@override
Widget build(BuildContext context) {
// Utilise exactement vos noms de fichiers
String logoPath =
customPath ??
(isWhite
? 'assets/images/wortis_logo_white.png'
: 'assets/images/wortis_logo.png');
return Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(size * 0.15),
boxShadow:
withShadow
? [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: size * 0.1,
offset: Offset(0, size * 0.05),
spreadRadius: 1,
),
]
: null,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(size * 0.15),
child: Image.asset(
logoPath,
width: size,
height: size,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
// Debug : Afficher l'erreur pour diagnostiquer
print('Erreur chargement logo: $error');
print('Chemin tenté: $logoPath');
// Fallback avec le "W" stylisé
return Container(
width: size,
height: size,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF006699), Color(0xFF0088CC)],
),
borderRadius: BorderRadius.circular(size * 0.15),
boxShadow:
withShadow
? [
BoxShadow(
color: Color(0xFF006699).withOpacity(0.3),
blurRadius: size * 0.15,
offset: Offset(0, size * 0.08),
),
]
: null,
),
);
},
),
),
);
}
}
/*
DIAGNOSTIC : Si les images ne s'affichent toujours pas, vérifiez :
1. pubspec.yaml contient bien :
flutter:
assets:
- assets/images/
2. Redémarrez l'app complètement (hot restart, pas hot reload)
3. Dans votre terminal, exécutez :
flutter clean
flutter pub get
flutter run
4. Vérifiez que les images sont bien au format PNG/JPG valide
5. Si ça ne marche toujours pas, essayez de renommer temporairement
vos fichiers en :
- logo1.png
- logo2.png
Et modifiez les chemins dans le code pour tester.
*/

1
linux/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
flutter/ephemeral

128
linux/CMakeLists.txt Normal file
View File

@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
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 "wtpe")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.wtpe")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -0,0 +1,19 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <flutter_window_close/flutter_window_close_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_window_close_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWindowClosePlugin");
flutter_window_close_plugin_register_with_registrar(flutter_window_close_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -0,0 +1,25 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
flutter_window_close
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

Some files were not shown because too many files have changed in this diff Show More