feat: Version 3.6.3 - Carte IGN, mode boussole, corrections Flutter analyze
Nouvelles fonctionnalités: - #215 Mode boussole + carte IGN/satellite (Mode terrain) - #53 Définition zoom maximal pour éviter sur-zoom - #14 Correction bug F5 déconnexion - #204 Design couleurs flashy - #205 Écrans utilisateurs simplifiés Corrections Flutter analyze: - Suppression warnings room.g.dart, chat_service.dart, api_service.dart - 0 error, 0 warning, 30 infos (suggestions de style) Autres: - Intégration tuiles IGN Plan et IGN Ortho (geopf.fr) - flutter_compass pour Android/iOS - Réorganisation assets store Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@@ -252,6 +252,21 @@ else
|
||||
fi
|
||||
echo
|
||||
|
||||
# Étape 2.5 : Patcher nfc_manager pour AGP 8+
|
||||
print_message "Étape 2.5/5 : Patch nfc_manager pour Android Gradle Plugin 8+..."
|
||||
NFC_PATCH_SCRIPT="./fastlane/scripts/commun/fix-nfc-manager.sh"
|
||||
if [ -f "$NFC_PATCH_SCRIPT" ]; then
|
||||
bash "$NFC_PATCH_SCRIPT"
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Patch nfc_manager appliqué"
|
||||
else
|
||||
print_warning "Le patch nfc_manager a échoué (peut être déjà appliqué)"
|
||||
fi
|
||||
else
|
||||
print_warning "Script de patch nfc_manager introuvable : $NFC_PATCH_SCRIPT"
|
||||
fi
|
||||
echo
|
||||
|
||||
# Étape 3 : Analyser le code (optionnel mais recommandé)
|
||||
print_message "Étape 3/5 : Analyse du code Dart..."
|
||||
flutter analyze --no-fatal-infos --no-fatal-warnings || {
|
||||
@@ -415,8 +430,10 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
read -p "Installer l'APK debug sur l'appareil connecté ? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
print_message "Désinstallation de l'ancienne version..."
|
||||
adb uninstall fr.geosector.app3 2>/dev/null || print_warning "Aucune version précédente trouvée"
|
||||
print_message "Installation sur l'appareil..."
|
||||
adb install -r "$APK_NAME"
|
||||
adb install "$APK_NAME"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "APK installé avec succès"
|
||||
|
||||
@@ -254,9 +254,13 @@ EOF
|
||||
echo_info "Fixing web assets structure..."
|
||||
./copy-web-images.sh || echo_error "Failed to fix web assets"
|
||||
|
||||
# Créer l'archive depuis le build
|
||||
# Créer l'archive depuis le build (avec exclusions pour réduire la taille)
|
||||
echo_info "Creating archive from build..."
|
||||
tar -czf "${TEMP_ARCHIVE}" -C ${FLUTTER_BUILD_DIR} . || echo_error "Failed to create archive"
|
||||
tar -czf "${TEMP_ARCHIVE}" -C ${FLUTTER_BUILD_DIR} \
|
||||
--exclude='*.symbols' \
|
||||
--exclude='*.kra' \
|
||||
--exclude='.DS_Store' \
|
||||
. || echo_error "Failed to create archive"
|
||||
|
||||
create_local_backup "${TEMP_ARCHIVE}" "dev"
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/connectivity_plus/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/connectivity_plus/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/connectivity_plus/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/connectivity_plus/example/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/connectivity_plus/example/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/connectivity_plus/example/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/path_provider_foundation/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/path_provider_foundation/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/path_provider_foundation/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/path_provider_foundation/example/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/path_provider_foundation/example/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/path_provider_foundation/example/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/url_launcher_ios/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/url_launcher_ios/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/url_launcher_ios/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/url_launcher_ios/example/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/url_launcher_ios/example/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ios/.symlinks/plugins/url_launcher_ios/example/build" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Flutter Plugins" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/core/services/theme_service.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
// Import conditionnel pour le web
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
@@ -45,10 +48,80 @@ class GeosectorApp extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GeosectorAppState extends State<GeosectorApp> with WidgetsBindingObserver {
|
||||
// Clé globale pour accéder au contexte de l'app (pour les dialogues)
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// Sur Web, intercepter F5 / Ctrl+R pour proposer un refresh des données
|
||||
if (kIsWeb) {
|
||||
_setupF5Interceptor();
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure l'interception de F5/Ctrl+R sur Web
|
||||
void _setupF5Interceptor() {
|
||||
html.window.onKeyDown.listen((event) {
|
||||
// Détecter F5 ou Ctrl+R
|
||||
final isF5 = event.key == 'F5';
|
||||
final isCtrlR = (event.ctrlKey || event.metaKey) && event.key?.toLowerCase() == 'r';
|
||||
|
||||
if (isF5 || isCtrlR) {
|
||||
event.preventDefault();
|
||||
debugPrint('🔄 F5/Ctrl+R intercepté - Affichage du dialogue de refresh');
|
||||
_showRefreshDialog();
|
||||
}
|
||||
});
|
||||
debugPrint('🌐 Intercepteur F5/Ctrl+R configuré pour Web');
|
||||
}
|
||||
|
||||
/// Affiche le dialogue de confirmation de refresh
|
||||
void _showRefreshDialog() {
|
||||
final context = navigatorKey.currentContext;
|
||||
if (context == null) {
|
||||
debugPrint('⚠️ Impossible d\'afficher le dialogue - contexte non disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.refresh, color: Colors.blue),
|
||||
SizedBox(width: 12),
|
||||
Text('Recharger les données ?'),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
'Voulez-vous actualiser vos données depuis le serveur ?\n\n'
|
||||
'Vos modifications non synchronisées seront conservées.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
debugPrint('❌ Refresh annulé par l\'utilisateur');
|
||||
},
|
||||
child: const Text('Non'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
debugPrint('✅ Refresh demandé par l\'utilisateur');
|
||||
// TODO: Implémenter le refresh des données via API
|
||||
},
|
||||
child: const Text('Oui, recharger'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -159,6 +232,7 @@ class _GeosectorAppState extends State<GeosectorApp> with WidgetsBindingObserver
|
||||
/// Création du routeur avec configuration pour URLs propres
|
||||
GoRouter _createRouter() {
|
||||
return GoRouter(
|
||||
navigatorKey: navigatorKey,
|
||||
initialLocation: '/',
|
||||
routes: [
|
||||
GoRoute(
|
||||
|
||||
@@ -26,7 +26,7 @@ class RoomAdapter extends TypeAdapter<Room> {
|
||||
unreadCount: fields[6] as int,
|
||||
recentMessages: (fields[7] as List?)
|
||||
?.map((dynamic e) => (e as Map).cast<String, dynamic>())
|
||||
?.toList(),
|
||||
.toList(),
|
||||
updatedAt: fields[8] as DateTime?,
|
||||
createdBy: fields[9] as int?,
|
||||
isSynced: fields[10] as bool,
|
||||
|
||||
@@ -28,10 +28,8 @@ class ChatService {
|
||||
|
||||
Timer? _syncTimer;
|
||||
DateTime? _lastSyncTimestamp;
|
||||
DateTime? _lastFullSync;
|
||||
static const Duration _syncInterval = Duration(seconds: 30); // Sync toutes les 30 secondes
|
||||
static const Duration _initialSyncDelay = Duration(seconds: 10); // Délai avant première sync
|
||||
static const Duration _fullSyncInterval = Duration(minutes: 5);
|
||||
|
||||
/// Initialisation avec gestion des rôles et configuration YAML
|
||||
static Future<void> init({
|
||||
|
||||
@@ -146,9 +146,9 @@ class AppKeys {
|
||||
1: {
|
||||
'titres': 'Effectués',
|
||||
'titre': 'Effectué',
|
||||
'couleur1': 0xFF00E09D, // Vert (Figma)
|
||||
'couleur2': 0xFF00E09D, // Vert (Figma)
|
||||
'couleur3': 0xFF00E09D, // Vert (Figma)
|
||||
'couleur1': 0xFF008000, // Vert foncé
|
||||
'couleur2': 0xFF008000, // Vert foncé
|
||||
'couleur3': 0xFF008000, // Vert foncé
|
||||
'icon_data': Icons.task_alt,
|
||||
},
|
||||
2: {
|
||||
@@ -170,9 +170,9 @@ class AppKeys {
|
||||
4: {
|
||||
'titres': 'Dons',
|
||||
'titre': 'Don',
|
||||
'couleur1': 0xFF395AA7, // Bleu (Figma)
|
||||
'couleur2': 0xFF395AA7, // Bleu (Figma)
|
||||
'couleur3': 0xFF395AA7, // Bleu (Figma)
|
||||
'couleur1': 0xFF00BCD4, // Cyan
|
||||
'couleur2': 0xFF00BCD4, // Cyan
|
||||
'couleur3': 0xFF00BCD4, // Cyan
|
||||
'icon_data': Icons.volunteer_activism,
|
||||
},
|
||||
5: {
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
@@ -69,10 +68,15 @@ class ApiService {
|
||||
_dio.options.headers.addAll(headers);
|
||||
|
||||
// Gestionnaire de cookies pour les sessions PHP
|
||||
// Utilise CookieJar en mémoire (cookies maintenus pendant la durée de vie de l'app)
|
||||
final cookieJar = CookieJar();
|
||||
_dio.interceptors.add(CookieManager(cookieJar));
|
||||
debugPrint('🍪 [API] Gestionnaire de cookies activé');
|
||||
// IMPORTANT: Désactivé sur Web car les navigateurs bloquent la manipulation des cookies via XHR
|
||||
// Sur Web, on utilise uniquement le header Authorization avec Bearer token
|
||||
if (!kIsWeb) {
|
||||
final cookieJar = CookieJar();
|
||||
_dio.interceptors.add(CookieManager(cookieJar));
|
||||
debugPrint('🍪 [API] Gestionnaire de cookies activé (mobile)');
|
||||
} else {
|
||||
debugPrint('🌐 [API] Mode Web - pas de CookieManager (Bearer token uniquement)');
|
||||
}
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
|
||||
// This file is automatically generated by deploy-app.sh script
|
||||
// Last update: 2026-01-16 13:37:45
|
||||
// Last update: 2026-01-19 15:35:06
|
||||
// Source: ../VERSION file
|
||||
//
|
||||
// GEOSECTOR App Version Service
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
class AppInfoService {
|
||||
// Version number (format: x.x.x)
|
||||
static const String version = '3.6.2';
|
||||
static const String version = '3.6.3';
|
||||
|
||||
// Build number (version without dots: xxx)
|
||||
static const String buildNumber = '362';
|
||||
static const String buildNumber = '363';
|
||||
|
||||
// Full version string (format: vx.x.x+xxx)
|
||||
static String get fullVersion => 'v$version+$buildNumber';
|
||||
|
||||
@@ -140,18 +140,43 @@ class CurrentUserService extends ChangeNotifier {
|
||||
|
||||
Future<void> loadFromHive() async {
|
||||
try {
|
||||
final box = Hive.box<UserModel>(AppKeys.userBoxName); // Nouvelle Box
|
||||
final user = box.get('current_user');
|
||||
// 1. Récupérer l'ID utilisateur depuis settings
|
||||
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
debugPrint('⚠️ Box settings non ouverte, impossible de charger l\'utilisateur');
|
||||
_currentUser = null;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
final userId = settingsBox.get('current_user_id');
|
||||
|
||||
if (userId == null) {
|
||||
debugPrint('ℹ️ Aucun current_user_id trouvé dans settings');
|
||||
_currentUser = null;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🔍 Recherche utilisateur avec ID: $userId');
|
||||
|
||||
// 2. Récupérer l'utilisateur avec le bon ID
|
||||
final box = Hive.box<UserModel>(AppKeys.userBoxName);
|
||||
final user = box.get(userId);
|
||||
|
||||
if (user?.hasValidSession == true) {
|
||||
_currentUser = user;
|
||||
debugPrint('📥 Utilisateur chargé depuis Hive: ${user?.email}');
|
||||
debugPrint('📥 Utilisateur chargé depuis Hive: ${user?.email} (ID: $userId)');
|
||||
|
||||
// Charger le mode d'affichage sauvegardé lors de la connexion
|
||||
await _loadDisplayMode();
|
||||
} else {
|
||||
_currentUser = null;
|
||||
debugPrint('ℹ️ Aucun utilisateur valide trouvé dans Hive');
|
||||
if (user == null) {
|
||||
debugPrint('ℹ️ Utilisateur ID $userId non trouvé dans la box');
|
||||
} else {
|
||||
debugPrint('ℹ️ Session expirée pour l\'utilisateur ${user.email}');
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
||||
@@ -50,6 +50,57 @@ class HiveService {
|
||||
|
||||
bool _isInitialized = false;
|
||||
bool get isInitialized => _isInitialized;
|
||||
|
||||
// === INITIALISATION LÉGÈRE POUR F5 (préserve les données) ===
|
||||
|
||||
/// Initialisation légère de Hive SANS destruction des données
|
||||
/// Utilisée pour le F5 sur Web afin de vérifier si une session existe
|
||||
/// Retourne true si l'initialisation a réussi et qu'une session utilisateur existe
|
||||
Future<bool> initializeWithoutReset() async {
|
||||
try {
|
||||
debugPrint('🔧 Initialisation légère de Hive (préservation des données)...');
|
||||
|
||||
// 1. Initialisation de base de Hive (idempotent)
|
||||
await Hive.initFlutter();
|
||||
debugPrint('✅ Hive.initFlutter() terminé');
|
||||
|
||||
// 2. Enregistrement des adaptateurs (idempotent)
|
||||
_registerAdapters();
|
||||
|
||||
// 3. Ouvrir les boxes SANS les détruire
|
||||
await _createAllBoxes();
|
||||
|
||||
// 4. Vérifier si une session utilisateur existe
|
||||
bool hasSession = false;
|
||||
try {
|
||||
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
final userId = settingsBox.get('current_user_id');
|
||||
if (userId != null) {
|
||||
// Vérifier que l'utilisateur existe dans la box user
|
||||
if (Hive.isBoxOpen(AppKeys.userBoxName)) {
|
||||
final userBox = Hive.box<UserModel>(AppKeys.userBoxName);
|
||||
final user = userBox.get(userId);
|
||||
if (user != null && user.hasValidSession) {
|
||||
hasSession = true;
|
||||
debugPrint('✅ Session utilisateur trouvée pour ID: $userId');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur vérification session: $e');
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
debugPrint('✅ Initialisation légère terminée, session existante: $hasSession');
|
||||
return hasSession;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de l\'initialisation légère: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// === INITIALISATION COMPLÈTE (appelée par main.dart) ===
|
||||
|
||||
/// Initialisation complète de Hive avec réinitialisation totale
|
||||
|
||||
@@ -313,13 +313,29 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
// Appeler le nouvel endpoint API pour restaurer la session
|
||||
// IMPORTANT: Utiliser getWithoutQueue() pour ne JAMAIS mettre cette requête en file d'attente
|
||||
// Les refresh de session sont liés à une session spécifique et ne doivent pas être rejoués
|
||||
debugPrint('🔄 Appel API: user/session avec sessionId: ${sessionId.substring(0, 10)}...');
|
||||
|
||||
final response = await ApiService.instance.getWithoutQueue(
|
||||
'/api/user/session',
|
||||
'user/session',
|
||||
queryParameters: {'mode': displayMode},
|
||||
);
|
||||
|
||||
// Gestion des codes de retour HTTP
|
||||
final statusCode = response.statusCode ?? 0;
|
||||
|
||||
// Vérifier que la réponse est bien du JSON et pas du HTML
|
||||
if (response.data is String) {
|
||||
final dataStr = response.data as String;
|
||||
if (dataStr.contains('<!DOCTYPE') || dataStr.contains('<html')) {
|
||||
debugPrint('❌ ERREUR: L\'API a retourné du HTML au lieu de JSON !');
|
||||
debugPrint('❌ StatusCode: $statusCode');
|
||||
debugPrint('❌ URL de base: ${ApiService.instance.baseUrl}');
|
||||
debugPrint('❌ Début de la réponse: ${dataStr.substring(0, 100)}...');
|
||||
await CurrentUserService.instance.clearUser();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final data = response.data as Map<String, dynamic>?;
|
||||
|
||||
switch (statusCode) {
|
||||
@@ -589,9 +605,9 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
_progress = 0.12;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200)); // Petit délai pour voir le début
|
||||
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Chargement des composants...";
|
||||
@@ -599,21 +615,68 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
});
|
||||
}
|
||||
|
||||
// Étape 2: Initialisation Hive - 15 à 60% (étape la plus longue)
|
||||
await HiveService.instance.initializeAndResetHive();
|
||||
|
||||
// === GESTION F5 WEB : Vérifier session AVANT de détruire les données ===
|
||||
// Sur Web, on essaie d'abord de récupérer une session existante
|
||||
if (kIsWeb) {
|
||||
debugPrint('🌐 Web détecté - tentative de récupération de session existante...');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Vérification de session...";
|
||||
_progress = 0.20;
|
||||
});
|
||||
}
|
||||
|
||||
// Initialisation légère qui préserve les données
|
||||
final hasExistingSession = await HiveService.instance.initializeWithoutReset();
|
||||
|
||||
if (hasExistingSession) {
|
||||
debugPrint('✅ Session existante détectée, tentative de restauration...');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Restauration de la session...";
|
||||
_progress = 0.40;
|
||||
});
|
||||
}
|
||||
|
||||
// Tenter la restauration via l'API
|
||||
final sessionRestored = await _handleSessionRefreshIfNeeded();
|
||||
if (sessionRestored) {
|
||||
debugPrint('✅ Session restaurée via F5 - fin de l\'initialisation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Si la restauration API échoue, on continue vers le login
|
||||
debugPrint('⚠️ Restauration API échouée, passage au login normal');
|
||||
} else {
|
||||
debugPrint('ℹ️ Pas de session existante, initialisation normale');
|
||||
}
|
||||
}
|
||||
|
||||
// === INITIALISATION NORMALE (si pas de session F5 ou pas Web) ===
|
||||
// Étape 2: Initialisation Hive complète - 15 à 60%
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Configuration du stockage...";
|
||||
_progress = 0.45;
|
||||
_progress = 0.30;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 300)); // Simulation du temps de traitement
|
||||
|
||||
|
||||
await HiveService.instance.initializeAndResetHive();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Préparation des données...";
|
||||
_progress = 0.45;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 300)); // Simulation du temps de traitement
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Ouverture des bases...";
|
||||
_progress = 0.60;
|
||||
});
|
||||
}
|
||||
@@ -621,19 +684,9 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
// Étape 3: Ouverture des Box - 60 à 80%
|
||||
await HiveService.instance.ensureBoxesAreOpen();
|
||||
|
||||
// NOUVEAU : Vérifier et nettoyer si nouvelle version (Web uniquement)
|
||||
// Maintenant que les boxes sont ouvertes, on peut vérifier la version dans Hive
|
||||
// Vérifier et nettoyer si nouvelle version (Web uniquement)
|
||||
await _checkVersionAndCleanIfNeeded();
|
||||
|
||||
// NOUVEAU : Détecter et gérer le F5 (refresh de page web avec session existante)
|
||||
final sessionRestored = await _handleSessionRefreshIfNeeded();
|
||||
if (sessionRestored) {
|
||||
// Session restaurée avec succès, on arrête ici
|
||||
// L'utilisateur a été redirigé vers son interface
|
||||
debugPrint('✅ Session restaurée via F5 - fin de l\'initialisation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Gérer la box pending_requests séparément pour préserver les données
|
||||
try {
|
||||
debugPrint('📦 Gestion de la box pending_requests...');
|
||||
|
||||
@@ -40,17 +40,19 @@ class _HomeContentState extends State<HomeContent> {
|
||||
final isDesktop = screenWidth > 800;
|
||||
|
||||
// Retourner seulement le contenu (sans scaffold)
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS,
|
||||
vertical: AppTheme.spacingL,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Widget BtnPassages
|
||||
const BtnPassages(),
|
||||
const SizedBox(height: AppTheme.spacingL),
|
||||
return Column(
|
||||
children: [
|
||||
// Widget BtnPassages collé en haut/gauche/droite
|
||||
const BtnPassages(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS,
|
||||
vertical: AppTheme.spacingL,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
// LIGNE 1 : Graphiques de répartition (type de passage et mode de paiement)
|
||||
isDesktop
|
||||
@@ -172,9 +174,12 @@ class _HomeContentState extends State<HomeContent> {
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Construit la carte de répartition par type de passage
|
||||
|
||||
@@ -123,6 +123,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
// État pour bloquer la sauvegarde du zoom lors du centrage sur secteur
|
||||
bool _isCenteringOnSector = false;
|
||||
|
||||
// Source des tuiles de la carte (IGN Plan ou IGN Ortho)
|
||||
TileSource _tileSource = TileSource.ignPlan;
|
||||
|
||||
// Comptages des secteurs (calculés uniquement lors de création/modification de secteurs)
|
||||
Map<int, int> _sectorPassageCount = {};
|
||||
Map<int, int> _sectorMemberCount = {};
|
||||
@@ -215,6 +218,16 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
_settingsBox.put('mapZoom', 15.0);
|
||||
debugPrint('🔍 MapPage: Aucun zoom sauvegardé, utilisation du défaut = 15.0');
|
||||
}
|
||||
|
||||
// Charger la source des tuiles (IGN Plan par défaut)
|
||||
final savedTileSource = _settingsBox.get('mapTileSource');
|
||||
if (savedTileSource != null) {
|
||||
_tileSource = TileSource.values.firstWhere(
|
||||
(t) => t.name == savedTileSource,
|
||||
orElse: () => TileSource.ignPlan,
|
||||
);
|
||||
debugPrint('🗺️ MapPage: Source tuiles chargée = $_tileSource');
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode pour gérer les changements de sélection de secteur
|
||||
@@ -4151,8 +4164,8 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
initialZoom: _currentZoom,
|
||||
mapController: _mapController,
|
||||
disableDrag: _isDraggingPoint,
|
||||
// Utiliser OpenStreetMap temporairement sur mobile si Mapbox échoue
|
||||
useOpenStreetMap: !kIsWeb, // true sur mobile, false sur web
|
||||
// Utiliser les tuiles IGN (Plan ou Ortho selon le choix utilisateur)
|
||||
tileSource: _tileSource,
|
||||
labelMarkers: _buildSectorLabels(),
|
||||
markers: [
|
||||
..._buildMarkers(),
|
||||
@@ -4199,14 +4212,38 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
),
|
||||
)),
|
||||
|
||||
// Boutons d'action en haut à droite (Web uniquement et admin seulement)
|
||||
if (kIsWeb && canEditSectors)
|
||||
Positioned(
|
||||
right: 16,
|
||||
top: 16,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// Bouton switch IGN Plan / Ortho en haut à droite (visible pour tous)
|
||||
Positioned(
|
||||
right: 16,
|
||||
top: 16,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// Bouton switch IGN Plan / Ortho
|
||||
// L'icône montre l'action (vers quoi on bascule), pas l'état actuel
|
||||
_buildActionButton(
|
||||
icon: _tileSource == TileSource.ignPlan
|
||||
? Icons.satellite_alt // En mode plan → afficher satellite pour basculer
|
||||
: Icons.map_outlined, // En mode ortho → afficher plan pour basculer
|
||||
tooltip: _tileSource == TileSource.ignPlan
|
||||
? 'Passer en vue satellite'
|
||||
: 'Passer en vue plan',
|
||||
color: Colors.white,
|
||||
iconColor: Colors.blueGrey[700],
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_tileSource = _tileSource == TileSource.ignPlan
|
||||
? TileSource.ignOrtho
|
||||
: TileSource.ignPlan;
|
||||
_settingsBox.put('mapTileSource', _tileSource.name);
|
||||
debugPrint('🗺️ MapPage: Source tuiles changée = $_tileSource');
|
||||
});
|
||||
},
|
||||
),
|
||||
// Espacement avant les boutons admin
|
||||
if (kIsWeb && canEditSectors) const SizedBox(height: 16),
|
||||
// Boutons admin (création, modification, suppression de secteurs)
|
||||
if (kIsWeb && canEditSectors) ...[
|
||||
// Bouton Créer
|
||||
_buildActionButton(
|
||||
icon: Icons.pentagon_outlined,
|
||||
@@ -4246,8 +4283,9 @@ class _MapPageContentState extends State<MapPageContent> {
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Menu contextuel (apparaît selon le mode) - Web uniquement et admin seulement
|
||||
if (kIsWeb && canEditSectors && _mapMode != MapMode.view)
|
||||
|
||||
@@ -8,13 +8,14 @@ import 'package:latlong2/latlong.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:flutter_compass/flutter_compass.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/services/current_amicale_service.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
|
||||
import 'package:geosector_app/presentation/widgets/grouped_passages_dialog.dart';
|
||||
import 'package:geosector_app/presentation/widgets/mapbox_map.dart' show TileSource;
|
||||
import 'package:geosector_app/app.dart';
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
|
||||
@@ -59,10 +60,20 @@ class _UserFieldModePageState extends State<UserFieldModePage>
|
||||
// Listener pour les changements de la box passages
|
||||
Box<PassageModel>? _passagesBox;
|
||||
|
||||
// Source des tuiles de la carte (IGN Plan ou IGN Ortho)
|
||||
TileSource _tileSource = TileSource.ignPlan;
|
||||
Box? _settingsBox;
|
||||
|
||||
// Mode boussole (Android/iOS uniquement)
|
||||
bool _compassModeEnabled = false;
|
||||
StreamSubscription<CompassEvent>? _compassSubscription;
|
||||
double _currentHeading = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
_loadTileSourceSetting();
|
||||
|
||||
// Écouter les changements de la Hive box passages pour rafraîchir la carte
|
||||
_passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
@@ -85,6 +96,26 @@ class _UserFieldModePageState extends State<UserFieldModePage>
|
||||
}
|
||||
}
|
||||
|
||||
// Charger le paramètre de source des tuiles depuis Hive
|
||||
Future<void> _loadTileSourceSetting() async {
|
||||
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
||||
_settingsBox = await Hive.openBox(AppKeys.settingsBoxName);
|
||||
} else {
|
||||
_settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
}
|
||||
|
||||
final savedTileSource = _settingsBox?.get('mapTileSource');
|
||||
if (savedTileSource != null && mounted) {
|
||||
setState(() {
|
||||
_tileSource = TileSource.values.firstWhere(
|
||||
(t) => t.name == savedTileSource,
|
||||
orElse: () => TileSource.ignPlan,
|
||||
);
|
||||
});
|
||||
debugPrint('FieldMode: Source tuiles chargée = $_tileSource');
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeWebMode() async {
|
||||
// Essayer d'obtenir la position réelle depuis le navigateur
|
||||
try {
|
||||
@@ -539,6 +570,7 @@ class _UserFieldModePageState extends State<UserFieldModePage>
|
||||
void dispose() {
|
||||
_positionStreamSubscription?.cancel();
|
||||
_qualityUpdateTimer?.cancel();
|
||||
_compassSubscription?.cancel();
|
||||
_gpsBlinkController.dispose();
|
||||
_networkBlinkController.dispose();
|
||||
_searchController.dispose();
|
||||
@@ -546,6 +578,35 @@ class _UserFieldModePageState extends State<UserFieldModePage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Activer/désactiver le mode boussole (Android/iOS uniquement)
|
||||
void _toggleCompassMode() {
|
||||
if (kIsWeb) return; // Pas de boussole sur web
|
||||
|
||||
setState(() {
|
||||
_compassModeEnabled = !_compassModeEnabled;
|
||||
});
|
||||
|
||||
if (_compassModeEnabled) {
|
||||
// Activer l'écoute de la boussole
|
||||
_compassSubscription = FlutterCompass.events?.listen((CompassEvent event) {
|
||||
if (event.heading != null && mounted) {
|
||||
setState(() {
|
||||
_currentHeading = event.heading!;
|
||||
});
|
||||
// Faire pivoter la carte selon la direction
|
||||
_mapController.rotate(-_currentHeading);
|
||||
}
|
||||
});
|
||||
debugPrint('FieldMode: Mode boussole activé');
|
||||
} else {
|
||||
// Désactiver l'écoute et remettre la carte vers le nord
|
||||
_compassSubscription?.cancel();
|
||||
_compassSubscription = null;
|
||||
_mapController.rotate(0);
|
||||
debugPrint('FieldMode: Mode boussole désactivé');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -823,10 +884,6 @@ class _UserFieldModePageState extends State<UserFieldModePage>
|
||||
);
|
||||
}
|
||||
|
||||
final apiService = ApiService.instance;
|
||||
final mapboxApiKey =
|
||||
AppKeys.getMapboxApiKey(apiService.getCurrentEnvironment());
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
@@ -837,21 +894,36 @@ class _UserFieldModePageState extends State<UserFieldModePage>
|
||||
initialZoom: 17,
|
||||
maxZoom: 19,
|
||||
minZoom: 10,
|
||||
interactionOptions: const InteractionOptions(
|
||||
interactionOptions: InteractionOptions(
|
||||
enableMultiFingerGestureRace: true,
|
||||
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
|
||||
// Permettre la rotation uniquement si le mode boussole est activé
|
||||
flags: _compassModeEnabled
|
||||
? InteractiveFlag.all
|
||||
: InteractiveFlag.all & ~InteractiveFlag.rotate,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
// Utiliser l'API v4 de Mapbox sur mobile ou OpenStreetMap en fallback
|
||||
urlTemplate: kIsWeb
|
||||
? 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=$mapboxApiKey'
|
||||
: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // OpenStreetMap temporairement sur mobile
|
||||
// Utiliser les tuiles IGN (Plan ou Ortho selon le choix utilisateur)
|
||||
urlTemplate: _tileSource == TileSource.ignOrtho
|
||||
? 'https://data.geopf.fr/wmts?'
|
||||
'REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0'
|
||||
'&TILEMATRIXSET=PM'
|
||||
'&LAYER=ORTHOIMAGERY.ORTHOPHOTOS'
|
||||
'&STYLE=normal'
|
||||
'&FORMAT=image/jpeg'
|
||||
'&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}'
|
||||
: 'https://data.geopf.fr/wmts?'
|
||||
'REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0'
|
||||
'&TILEMATRIXSET=PM'
|
||||
'&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2'
|
||||
'&STYLE=normal'
|
||||
'&FORMAT=image/png'
|
||||
'&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}',
|
||||
userAgentPackageName: 'app3.geosector.fr',
|
||||
additionalOptions: const {
|
||||
'attribution': '© OpenStreetMap contributors',
|
||||
},
|
||||
maxNativeZoom: 19,
|
||||
maxZoom: 20,
|
||||
minZoom: 7,
|
||||
),
|
||||
// Markers des passages
|
||||
MarkerLayer(
|
||||
@@ -900,6 +972,56 @@ class _UserFieldModePageState extends State<UserFieldModePage>
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
),
|
||||
// Boutons haut droite (IGN + Boussole)
|
||||
Positioned(
|
||||
top: 16,
|
||||
right: 16,
|
||||
child: Column(
|
||||
children: [
|
||||
// Bouton switch IGN Plan / Ortho
|
||||
FloatingActionButton.small(
|
||||
heroTag: 'tileSource',
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.green[700],
|
||||
tooltip: _tileSource == TileSource.ignPlan
|
||||
? 'Passer en vue satellite'
|
||||
: 'Passer en vue plan',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_tileSource = _tileSource == TileSource.ignPlan
|
||||
? TileSource.ignOrtho
|
||||
: TileSource.ignPlan;
|
||||
});
|
||||
// Sauvegarder le choix
|
||||
_settingsBox?.put('mapTileSource', _tileSource.name);
|
||||
debugPrint('FieldMode: Source tuiles = $_tileSource');
|
||||
},
|
||||
// L'icône montre l'action (vers quoi on bascule), pas l'état actuel
|
||||
child: Icon(
|
||||
_tileSource == TileSource.ignPlan
|
||||
? Icons.satellite_alt // En mode plan → afficher satellite pour basculer
|
||||
: Icons.map_outlined, // En mode ortho → afficher plan pour basculer
|
||||
),
|
||||
),
|
||||
// Bouton mode boussole (uniquement sur mobile)
|
||||
if (!kIsWeb) ...[
|
||||
const SizedBox(height: 8),
|
||||
FloatingActionButton.small(
|
||||
heroTag: 'compass',
|
||||
backgroundColor: _compassModeEnabled ? Colors.green[700] : Colors.white,
|
||||
foregroundColor: _compassModeEnabled ? Colors.white : Colors.green[700],
|
||||
tooltip: _compassModeEnabled
|
||||
? 'Désactiver le mode boussole'
|
||||
: 'Activer le mode boussole',
|
||||
onPressed: _toggleCompassMode,
|
||||
child: Icon(
|
||||
_compassModeEnabled ? Icons.explore : Icons.explore_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class BtnPassages extends StatelessWidget {
|
||||
final shouldShowLotType = _shouldShowLotType();
|
||||
|
||||
return SizedBox(
|
||||
height: 80,
|
||||
height: 92, // 80 + 12 pour le triangle indicateur
|
||||
width: double.infinity,
|
||||
child: ValueListenableBuilder<Box<PassageModel>>(
|
||||
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
||||
@@ -121,6 +121,7 @@ class BtnPassages extends StatelessWidget {
|
||||
/// Colonne TOTAL (cliquable, affiche tous les passages)
|
||||
Widget _buildTotalColumn(BuildContext context, int total) {
|
||||
final bool isSelected = selectedTypeId == null;
|
||||
final Color bgColor = Colors.grey[200]!;
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
@@ -147,55 +148,71 @@ class BtnPassages extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
border: Border.all(
|
||||
color: Colors.grey[400]!,
|
||||
width: isSelected ? 5 : 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppTheme.borderRadiusMedium),
|
||||
bottomLeft: Radius.circular(AppTheme.borderRadiusMedium),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.route,
|
||||
size: 20,
|
||||
color: Colors.black54,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
total.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
border: Border.all(
|
||||
color: Colors.grey[400]!,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppTheme.borderRadiusMedium),
|
||||
bottomLeft: Radius.circular(AppTheme.borderRadiusMedium),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.route,
|
||||
size: 20,
|
||||
color: Colors.black54,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
total.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
total > 1 ? 'passages' : 'passage',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
total > 1 ? 'passages' : 'passage',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
// Triangle indicateur de sélection
|
||||
if (isSelected)
|
||||
Center(
|
||||
child: CustomPaint(
|
||||
size: const Size(20, 12),
|
||||
painter: _TrianglePainter(color: bgColor),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -236,62 +253,78 @@ class BtnPassages extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: couleur.withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: couleur,
|
||||
width: isSelected ? 5 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
iconData,
|
||||
size: 20,
|
||||
color: couleur,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: couleur,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Text(
|
||||
titre,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
border: Border.all(
|
||||
color: couleur,
|
||||
width: 1,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
iconData,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: Text(
|
||||
titre,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Triangle indicateur de sélection
|
||||
if (isSelected)
|
||||
Center(
|
||||
child: CustomPaint(
|
||||
size: const Size(20, 12),
|
||||
painter: _TrianglePainter(color: couleur),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Colonne NOUVEAU PASSAGE (bouton +, fond vert)
|
||||
/// Colonne NOUVEAU PASSAGE (bouton +, fond blanc)
|
||||
Widget _buildAddColumn(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
@@ -302,47 +335,55 @@ class BtnPassages extends StatelessWidget {
|
||||
_showPassageFormDialog(context);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.buttonSuccessColor.withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: AppTheme.buttonSuccessColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(AppTheme.borderRadiusMedium),
|
||||
bottomRight: Radius.circular(AppTheme.borderRadiusMedium),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_circle_outline,
|
||||
size: 24,
|
||||
color: AppTheme.buttonSuccessColor,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Nouveau',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppTheme.buttonSuccessColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: Colors.grey[400]!,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(AppTheme.borderRadiusMedium),
|
||||
bottomRight: Radius.circular(AppTheme.borderRadiusMedium),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.add_circle_outline,
|
||||
size: 24,
|
||||
color: Colors.black87,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Nouveau',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Espace pour aligner avec les autres colonnes (pas de triangle sur ce bouton)
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -377,3 +418,30 @@ class BtnPassages extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// CustomPainter pour dessiner un triangle pointant vers le bas
|
||||
class _TrianglePainter extends CustomPainter {
|
||||
final Color color;
|
||||
|
||||
_TrianglePainter({required this.color});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final path = Path()
|
||||
..moveTo(0, 0) // Coin supérieur gauche
|
||||
..lineTo(size.width, 0) // Coin supérieur droit
|
||||
..lineTo(size.width / 2, size.height) // Pointe en bas au centre
|
||||
..close();
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _TrianglePainter oldDelegate) {
|
||||
return oldDelegate.color != color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,18 @@ import 'package:latlong2/latlong.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart'; // Import du service singleton
|
||||
|
||||
/// Enum représentant les différentes sources de tuiles disponibles
|
||||
enum TileSource {
|
||||
/// Tuiles Mapbox (par défaut)
|
||||
mapbox,
|
||||
/// Tuiles OpenStreetMap
|
||||
openStreetMap,
|
||||
/// Tuiles IGN Plan (carte routière française)
|
||||
ignPlan,
|
||||
/// Tuiles IGN Ortho Photos (photos aériennes)
|
||||
ignOrtho,
|
||||
}
|
||||
|
||||
/// Widget de carte réutilisable utilisant Mapbox
|
||||
///
|
||||
/// Ce widget encapsule un FlutterMap avec des tuiles Mapbox et fournit
|
||||
@@ -46,10 +58,14 @@ class MapboxMap extends StatefulWidget {
|
||||
|
||||
/// Désactive le drag de la carte
|
||||
final bool disableDrag;
|
||||
|
||||
|
||||
/// Utiliser OpenStreetMap au lieu de Mapbox (en cas de problème de token)
|
||||
@Deprecated('Utiliser tileSource à la place')
|
||||
final bool useOpenStreetMap;
|
||||
|
||||
/// Source des tuiles de la carte (Mapbox, OpenStreetMap, IGN Plan, IGN Ortho)
|
||||
final TileSource tileSource;
|
||||
|
||||
const MapboxMap({
|
||||
super.key,
|
||||
this.initialPosition = const LatLng(48.1173, -1.6778), // Rennes par défaut
|
||||
@@ -64,6 +80,7 @@ class MapboxMap extends StatefulWidget {
|
||||
this.mapStyle,
|
||||
this.disableDrag = false,
|
||||
this.useOpenStreetMap = false,
|
||||
this.tileSource = TileSource.mapbox,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -125,7 +142,7 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
_cacheInitialized = true;
|
||||
});
|
||||
}
|
||||
debugPrint('MapboxMap: Cache initialisé avec succès pour ${widget.useOpenStreetMap ? "OpenStreetMap" : "Mapbox"}');
|
||||
debugPrint('MapboxMap: Cache initialisé avec succès');
|
||||
} catch (e) {
|
||||
debugPrint('MapboxMap: Erreur lors de l\'initialisation du cache: $e');
|
||||
// En cas d'erreur, on continue sans cache
|
||||
@@ -175,30 +192,76 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Retourne l'URL template pour la source de tuiles sélectionnée
|
||||
String _getTileUrlTemplate() {
|
||||
// Rétrocompatibilité avec useOpenStreetMap
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
if (widget.useOpenStreetMap && widget.tileSource == TileSource.mapbox) {
|
||||
return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
}
|
||||
|
||||
switch (widget.tileSource) {
|
||||
case TileSource.openStreetMap:
|
||||
return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
|
||||
case TileSource.ignPlan:
|
||||
// IGN Plan IGN v2 - Carte routière française
|
||||
// Source: https://data.geopf.fr/wmts
|
||||
return 'https://data.geopf.fr/wmts?'
|
||||
'REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0'
|
||||
'&TILEMATRIXSET=PM'
|
||||
'&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2'
|
||||
'&STYLE=normal'
|
||||
'&FORMAT=image/png'
|
||||
'&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}';
|
||||
|
||||
case TileSource.ignOrtho:
|
||||
// IGN Ortho Photos - Photos aériennes
|
||||
// Source: https://data.geopf.fr/wmts
|
||||
return 'https://data.geopf.fr/wmts?'
|
||||
'REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0'
|
||||
'&TILEMATRIXSET=PM'
|
||||
'&LAYER=ORTHOIMAGERY.ORTHOPHOTOS'
|
||||
'&STYLE=normal'
|
||||
'&FORMAT=image/jpeg'
|
||||
'&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}';
|
||||
|
||||
case TileSource.mapbox:
|
||||
default:
|
||||
// Déterminer l'URL du template de tuiles Mapbox
|
||||
final String environment = ApiService.instance.getCurrentEnvironment();
|
||||
final String mapboxToken = AppKeys.getMapboxApiKey(environment);
|
||||
|
||||
if (kIsWeb) {
|
||||
return 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken';
|
||||
} else {
|
||||
return 'https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}@2x.png?access_token=$mapboxToken';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne le nom de la source de tuiles pour le debug
|
||||
String _getTileSourceName() {
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
if (widget.useOpenStreetMap && widget.tileSource == TileSource.mapbox) {
|
||||
return 'OpenStreetMap (legacy)';
|
||||
}
|
||||
switch (widget.tileSource) {
|
||||
case TileSource.mapbox:
|
||||
return 'Mapbox';
|
||||
case TileSource.openStreetMap:
|
||||
return 'OpenStreetMap';
|
||||
case TileSource.ignPlan:
|
||||
return 'IGN Plan';
|
||||
case TileSource.ignOrtho:
|
||||
return 'IGN Ortho Photos';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String urlTemplate;
|
||||
|
||||
if (widget.useOpenStreetMap) {
|
||||
// Utiliser OpenStreetMap comme alternative
|
||||
urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
debugPrint('MapboxMap: Utilisation d\'OpenStreetMap');
|
||||
} else {
|
||||
// Déterminer l'URL du template de tuiles Mapbox
|
||||
// Utiliser l'environnement actuel pour obtenir la bonne clé API
|
||||
final String environment = ApiService.instance.getCurrentEnvironment();
|
||||
final String mapboxToken = AppKeys.getMapboxApiKey(environment);
|
||||
|
||||
// Essayer différentes API Mapbox selon la plateforme
|
||||
if (kIsWeb) {
|
||||
// Sur web, on peut utiliser l'API styles
|
||||
urlTemplate = 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken';
|
||||
} else {
|
||||
// Sur mobile, utiliser l'API v4 qui fonctionne mieux avec les tokens standards
|
||||
// Format: mapbox.streets pour les rues, mapbox.satellite pour satellite
|
||||
urlTemplate = 'https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}@2x.png?access_token=$mapboxToken';
|
||||
}
|
||||
}
|
||||
final urlTemplate = _getTileUrlTemplate();
|
||||
debugPrint('MapboxMap: Utilisation de ${_getTileSourceName()}');
|
||||
|
||||
// Afficher un indicateur pendant l'initialisation du cache
|
||||
if (!_cacheInitialized) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_linux-0.9.3+2/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_linux-0.2.1+2/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_linux-2.2.1/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_linux-3.2.1/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/
|
||||
@@ -3,8 +3,8 @@ FLUTTER_ROOT=/home/pierre/.local/flutter
|
||||
FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app
|
||||
COCOAPODS_PARALLEL_CODE_SIGN=true
|
||||
FLUTTER_BUILD_DIR=build
|
||||
FLUTTER_BUILD_NAME=3.6.2
|
||||
FLUTTER_BUILD_NUMBER=362
|
||||
FLUTTER_BUILD_NAME=3.6.3
|
||||
FLUTTER_BUILD_NUMBER=363
|
||||
DART_OBFUSCATION=false
|
||||
TRACK_WIDGET_CREATION=true
|
||||
TREE_SHAKE_ICONS=false
|
||||
|
||||
@@ -4,8 +4,8 @@ export "FLUTTER_ROOT=/home/pierre/.local/flutter"
|
||||
export "FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app"
|
||||
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||
export "FLUTTER_BUILD_DIR=build"
|
||||
export "FLUTTER_BUILD_NAME=3.6.2"
|
||||
export "FLUTTER_BUILD_NUMBER=362"
|
||||
export "FLUTTER_BUILD_NAME=3.6.3"
|
||||
export "FLUTTER_BUILD_NUMBER=363"
|
||||
export "DART_OBFUSCATION=false"
|
||||
export "TRACK_WIDGET_CREATION=true"
|
||||
export "TREE_SHAKE_ICONS=false"
|
||||
|
||||
@@ -411,6 +411,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_compass:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_compass
|
||||
sha256: "1b4d7e6c95a675ec8482b5c9c9ccf1ebf0ced3dbec59dce28ad609da953de850"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.1"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: geosector_app
|
||||
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
|
||||
publish_to: 'none'
|
||||
version: 3.6.2+362
|
||||
version: 3.6.3+363
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@@ -48,7 +48,7 @@ dependencies:
|
||||
geolocator: ^13.0.3 # ⬇️ Downgrade depuis 14.0.2 (Flutter 3.24.5 LTS)
|
||||
geolocator_android: 4.6.1 # ✅ Force version sans toARGB32()
|
||||
universal_html: ^2.2.4 # Pour accéder à la localisation du navigateur (detection env)
|
||||
# sensors_plus: ^3.1.0 # ❌ SUPPRIMÉ - Mode boussole retiré (feature optionnelle peu utilisée) (13/10/2025)
|
||||
flutter_compass: ^0.8.1 # Mode boussole pour le mode terrain (Android/iOS uniquement)
|
||||
|
||||
# Chat et notifications
|
||||
# mqtt5_client: ^4.11.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: geosector_app
|
||||
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
|
||||
publish_to: 'none'
|
||||
version: 3.5.9+359
|
||||
version: 3.6.3+363
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@@ -48,7 +48,7 @@ dependencies:
|
||||
geolocator: ^13.0.3 # ⬇️ Downgrade depuis 14.0.2 (Flutter 3.24.5 LTS)
|
||||
geolocator_android: 4.6.1 # ✅ Force version sans toARGB32()
|
||||
universal_html: ^2.2.4 # Pour accéder à la localisation du navigateur (detection env)
|
||||
# sensors_plus: ^3.1.0 # ❌ SUPPRIMÉ - Mode boussole retiré (feature optionnelle peu utilisée) (13/10/2025)
|
||||
flutter_compass: ^0.8.1 # Mode boussole pour le mode terrain (Android/iOS uniquement)
|
||||
|
||||
# Chat et notifications
|
||||
# mqtt5_client: ^4.11.0
|
||||
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.0 MiB |
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_windows-0.9.3+4/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_windows-1.0.3/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-1.0.3/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_windows-0.2.5/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_windows-0.2.1+1/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_windows-2.3.0/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_windows-0.2.1/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/
|
||||
@@ -1 +1 @@
|
||||
/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_windows-3.1.4/
|
||||
/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.4/
|
||||