import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/nymea_models.dart'; import '../models/thing_category.dart'; import '../services/nymea_service.dart'; import '../theme/app_theme.dart'; import '../main.dart' show DrawerMenuButton; import 'category_overview_screen.dart'; // ───────────────────────────────────────────────────────────────────────────── // ThingsScreen — NIVEAU 1 // Grille 2 colonnes : une tuile par catégorie présente. // Chaque tuile affiche l'icône/nom de catégorie et un carousel des things. // ───────────────────────────────────────────────────────────────────────────── class ThingsScreen extends StatelessWidget { const ThingsScreen({super.key}); static const _orderedCats = [ ThingCategory.energy, ThingCategory.solar, ThingCategory.battery, ThingCategory.evCharger, ThingCategory.cars, ThingCategory.hvac, ThingCategory.lighting, ThingCategory.sensors, ThingCategory.network, ThingCategory.notifications, ThingCategory.weather, ThingCategory.media, ThingCategory.other, ]; @override Widget build(BuildContext context) { final service = context.watch(); final things = service.things.where((t) => t.id.isNotEmpty).toList(); final classes = service.thingClasses; // Groupement par catégorie final Map> grouped = {}; for (final t in things) { final cls = _classFor(t, classes); final cat = cls?.category ?? ThingCategory.other; grouped.putIfAbsent(cat, () => []).add(t); } final cats = _orderedCats.where((c) => grouped.containsKey(c)).toList(); return Scaffold( backgroundColor: AppTheme.backgroundGray, appBar: _buildAppBar(service, things.length), body: !service.isConnected ? _buildPlaceholder( icon: Icons.cloud_off_rounded, label: 'Non connecté à nymea', ) : things.isEmpty && !service.thingsLoaded ? const Center( child: CircularProgressIndicator(color: AppTheme.accentTeal)) : things.isEmpty ? _buildPlaceholder( icon: Icons.devices_other_rounded, label: 'Aucun appareil', ) : GridView.builder( padding: const EdgeInsets.fromLTRB(16, 12, 16, 100), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: 0.98, ), // Si les classes ne sont pas encore chargées → 1 tuile "Autres" itemCount: cats.isEmpty ? 1 : cats.length, itemBuilder: (context, i) { final cat = cats.isEmpty ? ThingCategory.other : cats[i]; final catThings = grouped[cat] ?? things; final info = categoryInfoMap[cat]!; return _CategoryTile( info: info, things: catThings, thingClasses: classes, onTap: () => Navigator.push( context, MaterialPageRoute( builder: (_) => CategoryOverviewScreen( info: info, things: catThings, thingClasses: classes, ), ), ), ); }, ), ); } AppBar _buildAppBar(NymeaService service, int count) { return AppBar( backgroundColor: AppTheme.backgroundGray, elevation: 0, leading: const DrawerMenuButton(), leadingWidth: 56, title: Row(children: [ const Text('Things', style: TextStyle( fontWeight: FontWeight.bold, color: AppTheme.textDark, fontSize: 20)), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppTheme.accentTeal.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Text('$count', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: AppTheme.accentTeal)), ), ]), actions: [ IconButton( icon: const Icon(Icons.refresh_rounded, color: AppTheme.textDark), onPressed: () => service.refresh(), ), ], ); } static Widget _buildPlaceholder( {required IconData icon, required String label}) => Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 64, color: Colors.grey[400]), const SizedBox(height: 12), Text(label, style: const TextStyle(color: AppTheme.textLight, fontSize: 16)), ]), ); static NymeaThingClass? _classFor( NymeaThing t, List classes) { try { return classes.firstWhere((c) => c.id == t.thingClassId); } catch (_) { return null; } } } // ───────────────────────────────────────────────────────────────────────────── // _CategoryTile — tuile d'une catégorie // // ┌────────────────────────────┐ // │ [icône catégorie] │ ← centrée, colorée // │ NOM CATÉGORIE (CAPS) │ // │────────────────────────────│ ← séparateur couleur catégorie // │ ● Thing name valeur │ ← carousel auto (3 s) si >1 thing // └────────────────────────────┘ // ───────────────────────────────────────────────────────────────────────────── class _CategoryTile extends StatefulWidget { final ThingCategoryInfo info; final List things; final List thingClasses; final VoidCallback onTap; const _CategoryTile({ required this.info, required this.things, required this.thingClasses, required this.onTap, }); @override State<_CategoryTile> createState() => _CategoryTileState(); } class _CategoryTileState extends State<_CategoryTile> { int _idx = 0; Timer? _timer; @override void initState() { super.initState(); if (widget.things.length > 1) { _timer = Timer.periodic(const Duration(seconds: 3), (_) { if (mounted) { setState( () => _idx = (_idx + 1) % widget.things.length); } }); } } @override void dispose() { _timer?.cancel(); super.dispose(); } NymeaThingClass? _cls(NymeaThing t) { try { return widget.thingClasses.firstWhere((c) => c.id == t.thingClassId); } catch (_) { return null; } } bool _isOnline(NymeaThing t, NymeaThingClass? cls) { final ct = cls?.stateTypes.where((s) => s.name.toLowerCase() == 'connected'); if (ct != null && ct.isNotEmpty) { final v = t.stateValue(ct.first.id); return v == true || v == 'true'; } return t.isSetupComplete; } String? _primaryValue(NymeaThing t, NymeaThingClass? cls) { final p = cls?.primaryStateType; if (p == null) return null; return p.formatValue(t.stateValue(p.id)); } @override Widget build(BuildContext context) { // Clamp _idx au cas où la liste shrink entre deux ticks du timer final safeIdx = _idx.clamp(0, widget.things.length - 1); final current = widget.things[safeIdx]; final cls = _cls(current); final online = _isOnline(current, cls); final value = _primaryValue(current, cls); final info = widget.info; return Material( color: AppTheme.cardWhite, borderRadius: BorderRadius.circular(AppTheme.cornerRadius), child: InkWell( onTap: widget.onTap, borderRadius: BorderRadius.circular(AppTheme.cornerRadius), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Haut : icône + nom catégorie ─────────────────────────────── Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 52, height: 52, decoration: BoxDecoration( color: info.color.withValues(alpha: 0.13), borderRadius: BorderRadius.circular(14), ), child: Icon(info.icon, color: info.color, size: 28), ), const SizedBox(height: 10), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Text( info.label.toUpperCase(), style: TextStyle( fontSize: 10, fontWeight: FontWeight.w800, color: AppTheme.textDark.withValues(alpha: 0.75), letterSpacing: 0.7, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), // ── Séparateur coloré ─────────────────────────────────────────── Container( height: 1, color: info.color.withValues(alpha: 0.25), ), // ── Bas : carousel des things ─────────────────────────────────── SizedBox( height: 52, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: AnimatedSwitcher( duration: const Duration(milliseconds: 350), transitionBuilder: (child, anim) => FadeTransition(opacity: anim, child: child), child: _CarouselRow( key: ValueKey(_idx), name: current.name, value: value, count: widget.things.length, index: _idx, isOnline: online, color: info.color, ), ), ), ), ], ), ), ); } } // ── Ligne carousel ──────────────────────────────────────────────────────────── class _CarouselRow extends StatelessWidget { final String name; final String? value; final int count; final int index; final bool isOnline; final Color color; const _CarouselRow({ super.key, required this.name, required this.value, required this.count, required this.index, required this.isOnline, required this.color, }); @override Widget build(BuildContext context) { return Row( children: [ // Pastille statut Container( width: 7, height: 7, decoration: BoxDecoration( color: isOnline ? color : Colors.grey[400], shape: BoxShape.circle, ), ), const SizedBox(width: 8), // Nom du thing Expanded( child: Text( name, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.textDark), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 4), // Valeur primaire — Flexible pour éviter l'overflow Flexible( fit: FlexFit.loose, child: Text( value ?? '—', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: isOnline ? color : AppTheme.textLight, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ); } } // ───────────────────────────────────────────────────────────────────────────── // ThingCard — conservé pour compatibilité externe // ───────────────────────────────────────────────────────────────────────────── class ThingCard extends StatelessWidget { final NymeaThing thing; final NymeaThingClass? thingClass; final ThingCategoryInfo categoryInfo; final VoidCallback onTap; const ThingCard({ super.key, required this.thing, required this.thingClass, required this.categoryInfo, required this.onTap, }); @override Widget build(BuildContext context) { final primary = thingClass?.primaryStateType; final value = primary?.formatValue(thing.stateValue(primary.id)); return ListTile( leading: Icon(categoryInfo.icon, color: categoryInfo.color), title: Text(thing.name), trailing: value != null ? Text(value, style: TextStyle(color: categoryInfo.color)) : null, onTap: onTap, ); } }