etm-powersync-app/lib/screens/ac_screen.dart
pakutz79 c19c9d1a98 feat: navigation drawer, EMS setup, scheduler, tarifs, paramètres app
- Drawer custom (overlay Stack) avec mode installateur PIN SHA-256
- GoRouter + ShellRoute : navigation préservée entre onglets
- 6 providers : NavigationProvider, InstallerModeProvider, AppSettingsProvider,
  EnergySetupProvider, SchedulerProvider, TariffProvider
- Écrans Energy Manager : RoleConfigFlow (3 étapes), Scheduler, Tarifs, Timeline
- Écrans Paramètres : Apparence, Écrans actifs, AppSettingsScreen
- DrawerMenuButton présent dans les 5 AppBars principaux
- Simulation : _thingClasses générées avec interfaces EMS pour filtrage des rôles
- Compteur solaire : ajout smartmeter aux interfaces compatibles
- Thème ETM (etm_theme.dart), ProLockBadge, widgets PowerBar/RoleCard/TimelineSlotCard
- Dépendances : go_router, shared_preferences, crypto, url_launcher

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:52:32 +01:00

336 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../main.dart' show DrawerMenuButton;
class ACScreen extends StatefulWidget {
const ACScreen({super.key});
@override
State<ACScreen> createState() => _ACScreenState();
}
class _ACScreenState extends State<ACScreen> {
final List<_ACZone> _zones = [
_ACZone(name: 'Salon', currentTemp: 19.5, targetTemp: 21, mode: ACMode.heat, isOn: true),
_ACZone(name: 'Chambre', currentTemp: 18.0, targetTemp: 19, mode: ACMode.cool, isOn: false),
_ACZone(name: 'Bureau', currentTemp: 20.0, targetTemp: 22, mode: ACMode.heat, isOn: true),
_ACZone(name: 'Cuisine', currentTemp: 21.5, targetTemp: 21, mode: ACMode.auto, isOn: false),
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundGray,
appBar: AppBar(
backgroundColor: AppTheme.backgroundGray,
elevation: 0,
leading: const DrawerMenuButton(),
leadingWidth: 56,
title: const Text(
'Climatisation / Chauffage',
style: TextStyle(fontWeight: FontWeight.bold, color: AppTheme.textDark),
),
actions: [
IconButton(
icon: const Icon(Icons.schedule, color: AppTheme.textDark),
onPressed: () {},
),
],
),
body: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _zones.length,
separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return _ZoneCard(
zone: _zones[index],
onChanged: (updated) {
setState(() => _zones[index] = updated);
},
);
},
),
);
}
}
enum ACMode { heat, cool, auto, fan }
class _ACZone {
final String name;
final double currentTemp;
final double targetTemp;
final ACMode mode;
final bool isOn;
const _ACZone({
required this.name,
required this.currentTemp,
required this.targetTemp,
required this.mode,
required this.isOn,
});
_ACZone copyWith({
double? targetTemp,
ACMode? mode,
bool? isOn,
}) =>
_ACZone(
name: name,
currentTemp: currentTemp,
targetTemp: targetTemp ?? this.targetTemp,
mode: mode ?? this.mode,
isOn: isOn ?? this.isOn,
);
}
class _ZoneCard extends StatelessWidget {
final _ACZone zone;
final ValueChanged<_ACZone> onChanged;
const _ZoneCard({required this.zone, required this.onChanged});
Color get _modeColor {
switch (zone.mode) {
case ACMode.heat:
return Colors.orange;
case ACMode.cool:
return Colors.cyan;
case ACMode.auto:
return AppTheme.primaryGreen;
case ACMode.fan:
return Colors.blueGrey;
}
}
IconData get _modeIcon {
switch (zone.mode) {
case ACMode.heat:
return Icons.whatshot_rounded;
case ACMode.cool:
return Icons.ac_unit_rounded;
case ACMode.auto:
return Icons.autorenew_rounded;
case ACMode.fan:
return Icons.air_rounded;
}
}
String get _modeLabel {
switch (zone.mode) {
case ACMode.heat:
return 'Chauffage';
case ACMode.cool:
return 'Clim';
case ACMode.auto:
return 'Auto';
case ACMode.fan:
return 'Ventilation';
}
}
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Header row
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: zone.isOn
? _modeColor.withValues(alpha:0.15)
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
_modeIcon,
color: zone.isOn ? _modeColor : Colors.grey,
size: 26,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(zone.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.textDark,
fontSize: 16)),
Text(
zone.isOn ? _modeLabel : 'Éteint',
style: TextStyle(
fontSize: 12,
color:
zone.isOn ? _modeColor : AppTheme.textLight),
),
],
),
),
Switch(
value: zone.isOn,
onChanged: (v) => onChanged(zone.copyWith(isOn: v)),
activeThumbColor: _modeColor,
),
],
),
if (zone.isOn) ...[
const Divider(height: 20),
// Temperature
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
const Text('Actuelle',
style: TextStyle(
fontSize: 12, color: AppTheme.textLight)),
Text(
'${zone.currentTemp.toStringAsFixed(1)}°C',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppTheme.textDark,
),
),
],
),
Icon(Icons.arrow_forward_rounded,
color: Colors.grey.shade400),
Column(
children: [
const Text('Cible',
style: TextStyle(
fontSize: 12, color: AppTheme.textLight)),
Row(
children: [
IconButton(
icon: Icon(Icons.remove_circle_outline,
color: _modeColor),
onPressed: () => onChanged(zone.copyWith(
targetTemp: zone.targetTemp - 0.5)),
),
Text(
'${zone.targetTemp.toStringAsFixed(1)}°C',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: _modeColor,
),
),
IconButton(
icon: Icon(Icons.add_circle_outline,
color: _modeColor),
onPressed: () => onChanged(zone.copyWith(
targetTemp: zone.targetTemp + 0.5)),
),
],
),
],
),
],
),
// Mode selector
const SizedBox(height: 8),
Row(
children: ACMode.values.map((m) {
final selected = zone.mode == m;
final color = _modeColorFor(m);
final icon = _modeIconFor(m);
final label = _modeLabelFor(m);
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector(
onTap: () => onChanged(zone.copyWith(mode: m)),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: selected
? color.withValues(alpha:0.15)
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: selected
? Border.all(color: color, width: 1.5)
: null,
),
child: Column(
children: [
Icon(icon,
size: 18,
color: selected ? color : Colors.grey),
const SizedBox(height: 2),
Text(label,
style: TextStyle(
fontSize: 10,
color:
selected ? color : AppTheme.textLight,
fontWeight: selected
? FontWeight.bold
: FontWeight.normal)),
],
),
),
),
),
);
}).toList(),
),
],
],
),
),
);
}
Color _modeColorFor(ACMode m) {
switch (m) {
case ACMode.heat:
return Colors.orange;
case ACMode.cool:
return Colors.cyan;
case ACMode.auto:
return AppTheme.primaryGreen;
case ACMode.fan:
return Colors.blueGrey;
}
}
IconData _modeIconFor(ACMode m) {
switch (m) {
case ACMode.heat:
return Icons.whatshot_rounded;
case ACMode.cool:
return Icons.ac_unit_rounded;
case ACMode.auto:
return Icons.autorenew_rounded;
case ACMode.fan:
return Icons.air_rounded;
}
}
String _modeLabelFor(ACMode m) {
switch (m) {
case ACMode.heat:
return 'Chauf.';
case ACMode.cool:
return 'Clim';
case ACMode.auto:
return 'Auto';
case ACMode.fan:
return 'Vent.';
}
}
}