- 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>
233 lines
6.7 KiB
Dart
Executable File
233 lines
6.7 KiB
Dart
Executable File
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,
|
|
}
|