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>
This commit is contained in:
2026-01-19 17:46:03 +01:00
parent 232940b1eb
commit 5b6808db25
62 changed files with 1428 additions and 3130 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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(

View File

@@ -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,

View File

@@ -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({

View File

@@ -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: {

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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();

View File

@@ -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

View File

@@ -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...');

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
),
),
],
],
),
),
],
);
}

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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

View File

@@ -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"

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

View File

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 305 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

View File

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

View File

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 245 KiB

View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/