import 'package:flutter/material.dart'; import '../theme/app_theme.dart'; // ───────────────────────────────────────────────────────────────────────────── // NymeaTile — tuile carrée inspirée de MainPageTile.qml (nymea-app) // // Caractéristiques : // • Fond tileBackground (légèrement teinté, comme nymea) // • Effet glow animé sur press (BoxShadow colorée → simule Glow{} de Qt) // • Icône + pastille de statut en haut // • Valeur principale (grande, colorée) au bas // • Nom du thing tout en bas // ───────────────────────────────────────────────────────────────────────────── class NymeaTile extends StatefulWidget { /// Widget icône affiché en haut à gauche (généralement un [_TileIcon]) final Widget iconWidget; /// Nom principal affiché en bas de la tuile final String title; /// Valeur d'état primaire (ex. "3.47 kW", "21.5 °C", "On") final String? primaryValue; /// Sous-titre optionnel (ex. nom de classe) final String? subtitle; /// L'appareil est-il en ligne ? Influence couleur icône et glow final bool isOnline; /// Couleur d'accent de la catégorie (utilisée pour l'icône + glow + valeur) final Color accentColor; /// Widget optionnel en haut à droite (sinon : pastille de statut) final Widget? trailing; final VoidCallback? onTap; final VoidCallback? onLongPress; const NymeaTile({ super.key, required this.iconWidget, required this.title, this.primaryValue, this.subtitle, this.isOnline = true, this.accentColor = AppTheme.accentTeal, this.trailing, this.onTap, this.onLongPress, }); @override State createState() => _NymeaTileState(); } class _NymeaTileState extends State with SingleTickerProviderStateMixin { late final AnimationController _ctrl; late final Animation _glow; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 120), ); _glow = CurvedAnimation(parent: _ctrl, curve: Curves.easeOut); } @override void dispose() { _ctrl.dispose(); super.dispose(); } void _onTapDown(TapDownDetails _) => _ctrl.forward(); void _onTapUp(TapUpDetails _) { _ctrl.reverse(); widget.onTap?.call(); } void _onTapCancel() => _ctrl.reverse(); @override Widget build(BuildContext context) { return GestureDetector( onTapDown: _onTapDown, onTapUp: _onTapUp, onTapCancel: _onTapCancel, onLongPress: widget.onLongPress, child: AnimatedBuilder( animation: _glow, builder: (_, child) => Container( decoration: BoxDecoration( color: AppTheme.tileBackground, borderRadius: BorderRadius.circular(AppTheme.cornerRadius), boxShadow: [ // Ombre de base (toujours présente) BoxShadow( color: Colors.black.withValues(alpha: 0.07), blurRadius: 6, offset: const Offset(0, 2), ), // Glow coloré sur press (simule Glow{} de nymea-app) BoxShadow( color: widget.accentColor .withValues(alpha: 0.45 * _glow.value), blurRadius: 14 * _glow.value, spreadRadius: 2 * _glow.value, ), ], ), child: child, ), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Header : icône + pastille statut ──────────────────────── Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ widget.iconWidget, widget.trailing ?? _StatusDot( isOnline: widget.isOnline, color: widget.accentColor, ), ], ), const Spacer(), // ── Valeur principale ──────────────────────────────────────── if (widget.primaryValue != null) Text( widget.primaryValue!, style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: widget.isOnline ? widget.accentColor : AppTheme.textLight, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), // ── Nom du thing ───────────────────────────────────────────── Text( widget.title, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppTheme.textDark, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), // ── Sous-titre (classe) ────────────────────────────────────── if (widget.subtitle != null) Text( widget.subtitle!, style: const TextStyle( fontSize: 10, color: AppTheme.textLight, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ), ); } } // ─── Pastille de statut ─────────────────────────────────────────────────────── class _StatusDot extends StatelessWidget { final bool isOnline; final Color color; const _StatusDot({required this.isOnline, required this.color}); @override Widget build(BuildContext context) => Container( width: 9, height: 9, margin: const EdgeInsets.only(top: 2), decoration: BoxDecoration( color: isOnline ? color : Colors.grey[400], shape: BoxShape.circle, boxShadow: isOnline ? [ BoxShadow( color: color.withValues(alpha: 0.5), blurRadius: 4, spreadRadius: 1, ) ] : null, ), ); } // ─── TileIcon — icône dans container arrondi (helper) ──────────────────────── /// Icône standard pour une NymeaTile. /// Usage : `TileIcon(icon: Icons.wb_sunny, color: AppTheme.solarYellow)` class TileIcon extends StatelessWidget { final IconData icon; final Color color; final bool isOnline; const TileIcon({ super.key, required this.icon, required this.color, this.isOnline = true, }); @override Widget build(BuildContext context) => Container( width: 40, height: 40, decoration: BoxDecoration( color: color.withValues(alpha: isOnline ? 0.15 : 0.07), borderRadius: BorderRadius.circular(9), ), child: Icon( icon, color: isOnline ? color : color.withValues(alpha: 0.4), size: 22, ), ); }