feat: Gestion des secteurs et migration v3.0.4+304
- Ajout système complet de gestion des secteurs avec contours géographiques - Import des contours départementaux depuis GeoJSON - API REST pour la gestion des secteurs (/api/sectors) - Service de géolocalisation pour déterminer les secteurs - Migration base de données avec tables x_departements_contours et sectors_adresses - Interface Flutter pour visualisation et gestion des secteurs - Ajout thème sombre dans l'application - Corrections diverses et optimisations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
232
app/lib/presentation/widgets/theme_switcher.dart
Executable file
232
app/lib/presentation/widgets/theme_switcher.dart
Executable file
@@ -0,0 +1,232 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/services/theme_service.dart';
|
||||
|
||||
/// Widget pour basculer entre les thèmes clair/sombre/automatique
|
||||
class ThemeSwitcher extends StatelessWidget {
|
||||
/// Style d'affichage du sélecteur
|
||||
final ThemeSwitcherStyle style;
|
||||
|
||||
/// Afficher le texte descriptif
|
||||
final bool showLabel;
|
||||
|
||||
/// Callback optionnel appelé après changement de thème
|
||||
final VoidCallback? onThemeChanged;
|
||||
|
||||
const ThemeSwitcher({
|
||||
super.key,
|
||||
this.style = ThemeSwitcherStyle.iconButton,
|
||||
this.showLabel = false,
|
||||
this.onThemeChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: ThemeService.instance,
|
||||
builder: (context, child) {
|
||||
switch (style) {
|
||||
case ThemeSwitcherStyle.iconButton:
|
||||
return _buildIconButton(context);
|
||||
case ThemeSwitcherStyle.dropdown:
|
||||
return _buildDropdown(context);
|
||||
case ThemeSwitcherStyle.segmentedButton:
|
||||
return _buildSegmentedButton(context);
|
||||
case ThemeSwitcherStyle.toggleButtons:
|
||||
return _buildToggleButtons(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Bouton icône simple (bascule entre clair/sombre)
|
||||
Widget _buildIconButton(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(themeService.themeModeIcon),
|
||||
tooltip: 'Changer le thème (${themeService.themeModeDescription})',
|
||||
onPressed: () async {
|
||||
await themeService.toggleTheme();
|
||||
onThemeChanged?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Dropdown avec toutes les options
|
||||
Widget _buildDropdown(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return DropdownButton<ThemeMode>(
|
||||
value: themeService.themeMode,
|
||||
icon: Icon(Icons.arrow_drop_down, color: theme.colorScheme.onSurface),
|
||||
underline: Container(),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.system,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.brightness_auto, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Automatique'),
|
||||
if (showLabel) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'(${themeService.isSystemDark ? 'sombre' : 'clair'})',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const DropdownMenuItem(
|
||||
value: ThemeMode.light,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.light_mode, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Clair'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const DropdownMenuItem(
|
||||
value: ThemeMode.dark,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.dark_mode, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Sombre'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (ThemeMode? mode) async {
|
||||
if (mode != null) {
|
||||
await themeService.setThemeMode(mode);
|
||||
onThemeChanged?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Boutons segmentés (Material 3)
|
||||
Widget _buildSegmentedButton(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
return SegmentedButton<ThemeMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: ThemeMode.light,
|
||||
icon: Icon(Icons.light_mode, size: 16),
|
||||
label: Text('Clair'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.system,
|
||||
icon: Icon(Icons.brightness_auto, size: 16),
|
||||
label: Text('Auto'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.dark,
|
||||
icon: Icon(Icons.dark_mode, size: 16),
|
||||
label: Text('Sombre'),
|
||||
),
|
||||
],
|
||||
selected: {themeService.themeMode},
|
||||
onSelectionChanged: (Set<ThemeMode> selection) async {
|
||||
if (selection.isNotEmpty) {
|
||||
await themeService.setThemeMode(selection.first);
|
||||
onThemeChanged?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Boutons à bascule
|
||||
Widget _buildToggleButtons(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ToggleButtons(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
constraints: const BoxConstraints(minHeight: 40, minWidth: 60),
|
||||
isSelected: [
|
||||
themeService.themeMode == ThemeMode.light,
|
||||
themeService.themeMode == ThemeMode.system,
|
||||
themeService.themeMode == ThemeMode.dark,
|
||||
],
|
||||
onPressed: (int index) async {
|
||||
final modes = [ThemeMode.light, ThemeMode.system, ThemeMode.dark];
|
||||
await themeService.setThemeMode(modes[index]);
|
||||
onThemeChanged?.call();
|
||||
},
|
||||
children: const [
|
||||
Icon(Icons.light_mode, size: 20),
|
||||
Icon(Icons.brightness_auto, size: 20),
|
||||
Icon(Icons.dark_mode, size: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget d'information sur le thème actuel
|
||||
class ThemeInfo extends StatelessWidget {
|
||||
const ThemeInfo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: ThemeService.instance,
|
||||
builder: (context, child) {
|
||||
final themeService = ThemeService.instance;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
themeService.themeModeIcon,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
themeService.themeModeDescription,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Styles d'affichage pour le ThemeSwitcher
|
||||
enum ThemeSwitcherStyle {
|
||||
/// Bouton icône simple qui bascule entre clair/sombre
|
||||
iconButton,
|
||||
|
||||
/// Menu déroulant avec toutes les options
|
||||
dropdown,
|
||||
|
||||
/// Boutons segmentés (Material 3)
|
||||
segmentedButton,
|
||||
|
||||
/// Boutons à bascule
|
||||
toggleButtons,
|
||||
}
|
||||
Reference in New Issue
Block a user