import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gimnasio_soma/core/services/soma_logger.dart'; import 'package:gimnasio_soma/core/theme/soma_colors.dart'; import 'package:gimnasio_soma/core/widgets/soma_toast.dart'; class LogsScreen extends StatefulWidget { const LogsScreen({super.key}); @override State createState() => _LogsScreenState(); } class _LogsScreenState extends State { LogLevel? _filter; String _search = ''; List get _filteredEntries { var entries = SomaLogger.instance.entries.reversed.toList(); if (_filter != null) { entries = entries.where((e) => e.level == _filter).toList(); } if (_search.isNotEmpty) { final q = _search.toLowerCase(); entries = entries .where((e) => e.message.toLowerCase().contains(q) || e.tag.toLowerCase().contains(q) || (e.detail?.toLowerCase().contains(q) ?? false)) .toList(); } return entries; } void _copyAll() { final text = SomaLogger.instance.export(); Clipboard.setData(ClipboardData(text: text)); SomaToast.show(context, message: 'Logs copiados al portapapeles', type: ToastType.success); } void _clearLogs() { SomaLogger.instance.clear(); setState(() {}); SomaToast.show(context, message: 'Logs limpiados', type: ToastType.info); } @override Widget build(BuildContext context) { final isWide = MediaQuery.of(context).size.width >= 800; final theme = Theme.of(context); final entries = _filteredEntries; return Scaffold( body: Column( children: [ // Header Padding( padding: EdgeInsets.fromLTRB( isWide ? 32 : 16, isWide ? 28 : 16, isWide ? 32 : 16, 8, ), child: Column( children: [ Row( children: [ const Text( 'Logs', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: SomaColors.primary.withAlpha(25), borderRadius: BorderRadius.circular(6), ), child: Text( SomaLogger.instance.platform, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: theme.colorScheme.onSurface, ), ), ), const Spacer(), IconButton( onPressed: _copyAll, icon: const Icon(Icons.copy, size: 20), tooltip: 'Copiar logs', ), IconButton( onPressed: _clearLogs, icon: const Icon(Icons.delete_outline, size: 20), tooltip: 'Limpiar logs', ), IconButton( onPressed: () => setState(() {}), icon: const Icon(Icons.refresh, size: 20), tooltip: 'Refrescar', ), ], ), const SizedBox(height: 8), // Filtros Row( children: [ Expanded( child: SizedBox( height: 36, child: TextField( onChanged: (v) => setState(() => _search = v), decoration: InputDecoration( hintText: 'Buscar...', prefixIcon: const Icon(Icons.search, size: 18), contentPadding: const EdgeInsets.symmetric(horizontal: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: theme.colorScheme.surfaceContainerHighest, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: theme.colorScheme.surfaceContainerHighest, ), ), ), style: const TextStyle(fontSize: 13), ), ), ), const SizedBox(width: 8), _FilterChip( label: 'Todos', selected: _filter == null, onTap: () => setState(() => _filter = null), ), const SizedBox(width: 4), _FilterChip( label: 'RPC', selected: _search == 'RPC', onTap: () => setState(() { _search = _search == 'RPC' ? '' : 'RPC'; }), color: SomaColors.primary, ), const SizedBox(width: 4), _FilterChip( label: 'Error', selected: _filter == LogLevel.error, onTap: () => setState(() { _filter = _filter == LogLevel.error ? null : LogLevel.error; }), color: SomaColors.error, ), ], ), ], ), ), // Contador Padding( padding: EdgeInsets.symmetric(horizontal: isWide ? 32 : 16), child: Align( alignment: Alignment.centerLeft, child: Text( '${entries.length} entradas', style: TextStyle( fontSize: 11, color: theme.colorScheme.onSurface.withAlpha(100), ), ), ), ), const SizedBox(height: 4), // Lista Expanded( child: entries.isEmpty ? Center( child: Text( 'Sin logs', style: TextStyle( fontSize: 14, color: theme.colorScheme.onSurface.withAlpha(100), ), ), ) : ListView.builder( padding: EdgeInsets.fromLTRB( isWide ? 32 : 12, 0, isWide ? 32 : 12, 80, ), itemCount: entries.length, itemBuilder: (context, index) { return _LogEntryTile(entry: entries[index]); }, ), ), ], ), ); } } class _LogEntryTile extends StatelessWidget { final LogEntry entry; const _LogEntryTile({required this.entry}); Color _levelColor() { switch (entry.level) { case LogLevel.debug: return Colors.grey; case LogLevel.info: return Colors.blue; case LogLevel.warning: return Colors.orange; case LogLevel.error: return SomaColors.error; } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final ts = '${entry.timestamp.hour.toString().padLeft(2, '0')}:' '${entry.timestamp.minute.toString().padLeft(2, '0')}:' '${entry.timestamp.second.toString().padLeft(2, '0')}'; final dur = entry.duration != null ? ' ${entry.duration!.inMilliseconds}ms' : ''; return Padding( padding: const EdgeInsets.only(bottom: 2), child: InkWell( borderRadius: BorderRadius.circular(6), onTap: entry.detail != null ? () => _showDetail(context) : null, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: entry.level == LogLevel.error ? SomaColors.error.withAlpha(8) : Colors.transparent, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Timestamp Text( ts, style: TextStyle( fontSize: 11, fontFamily: 'monospace', color: theme.colorScheme.onSurface.withAlpha(80), ), ), const SizedBox(width: 6), // Level badge Container( width: 6, height: 6, margin: const EdgeInsets.only(top: 5), decoration: BoxDecoration( shape: BoxShape.circle, color: _levelColor(), ), ), const SizedBox(width: 6), // Tag Text( '[${entry.tag}]', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, fontFamily: 'monospace', color: theme.colorScheme.onSurface.withAlpha(140), ), ), const SizedBox(width: 6), // Message Expanded( child: Text( entry.message, style: TextStyle( fontSize: 12, fontFamily: 'monospace', color: theme.colorScheme.onSurface, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), // Duration if (dur.isNotEmpty) Text( dur, style: TextStyle( fontSize: 11, fontFamily: 'monospace', fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface.withAlpha(100), ), ), // Detail indicator if (entry.detail != null) Padding( padding: const EdgeInsets.only(left: 4), child: Icon( Icons.info_outline, size: 14, color: theme.colorScheme.onSurface.withAlpha(60), ), ), ], ), ), ), ); } void _showDetail(BuildContext context) { showDialog( context: context, builder: (ctx) => AlertDialog( title: Text( '[${entry.tag}] ${entry.message}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), ), content: SingleChildScrollView( child: SelectableText( entry.detail ?? '', style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), ), ), actions: [ TextButton( onPressed: () { Clipboard.setData( ClipboardData(text: '${entry.formatted}\n${entry.detail}')); Navigator.of(ctx).pop(); }, child: const Text('Copiar'), ), TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cerrar'), ), ], ), ); } } class _FilterChip extends StatelessWidget { final String label; final bool selected; final VoidCallback onTap; final Color? color; const _FilterChip({ required this.label, required this.selected, required this.onTap, this.color, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final c = color ?? SomaColors.primary; return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: selected ? c.withAlpha(25) : Colors.transparent, border: Border.all( color: selected ? c : theme.colorScheme.surfaceContainerHighest, width: 0.8, ), ), child: Text( label, style: TextStyle( fontSize: 11, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, color: selected ? c : theme.colorScheme.onSurface.withAlpha(130), ), ), ), ); } }