import 'package:flutter/material.dart'; import 'package:gamer_updater/game.dart'; import 'package:image_picker/image_picker.dart'; class GameCard extends StatefulWidget { final Game game; final Function(Game) onGameUpdated; final Function()? onDelete; final bool isNameEditable; const GameCard({ super.key, required this.game, required this.onGameUpdated, this.onDelete, this.isNameEditable = false, }); @override State createState() => _GameCardState(); } class _GameCardState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; bool _isLoading = false; int _deleteClickCount = 0; late TextEditingController _nameController; late TextEditingController _versionRegexController; late TextEditingController _rssFeedUrlController; late TextEditingController _lastPlayedController; late FocusNode _nameFocus; late FocusNode _versionRegexFocus; late FocusNode _rssFeedUrlFocus; late FocusNode _lastPlayedFocus; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 1), vsync: this, ); _nameController = TextEditingController(text: widget.game.name); _versionRegexController = TextEditingController( text: widget.game.versionRegex, ); _rssFeedUrlController = TextEditingController(text: widget.game.rssFeedUrl); _lastPlayedController = TextEditingController(text: widget.game.lastPlayed); _nameFocus = FocusNode(); _versionRegexFocus = FocusNode(); _rssFeedUrlFocus = FocusNode(); _lastPlayedFocus = FocusNode(); _setupFocusListeners(); } void _setupFocusListeners() { void updateGame() { try { var name = widget.isNameEditable ? _nameController.text : widget.game.name; if (name.isNotEmpty) { widget.onGameUpdated( Game( name: name, versionRegex: _versionRegexController.text, lastPlayed: _lastPlayedController.text, rssFeedUrl: _rssFeedUrlController.text, actualVersion: widget.game.actualVersion, lastUpdated: widget.game.lastUpdated, imageData: widget.game.imageData, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(e.toString()))); } } } _nameFocus.addListener(() { if (!_nameFocus.hasFocus) updateGame(); }); _versionRegexFocus.addListener(() { if (!_versionRegexFocus.hasFocus) updateGame(); }); _rssFeedUrlFocus.addListener(() { if (!_rssFeedUrlFocus.hasFocus) updateGame(); }); _lastPlayedFocus.addListener(() { if (!_lastPlayedFocus.hasFocus) updateGame(); }); } @override void dispose() { _controller.dispose(); _nameController.dispose(); _versionRegexController.dispose(); _rssFeedUrlController.dispose(); _lastPlayedController.dispose(); _nameFocus.dispose(); _versionRegexFocus.dispose(); _rssFeedUrlFocus.dispose(); _lastPlayedFocus.dispose(); super.dispose(); } Future _refreshVersion() async { setState(() => _isLoading = true); _controller.repeat(); final updatedGame = widget.game; try { await updatedGame.updateActualVersion(); } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(e.toString()))); } } widget.onGameUpdated(updatedGame); _controller.stop(); _controller.reset(); setState(() => _isLoading = false); } void _handleDeleteClick() { setState(() { _deleteClickCount++; if (_deleteClickCount >= 2) { widget.onDelete?.call(); _deleteClickCount = 0; } }); // Reset click count after a delay Future.delayed(const Duration(seconds: 1), () { if (mounted) { setState(() => _deleteClickCount = 0); } }); } Future _pickImage() async { final picker = ImagePicker(); final image = await picker.pickImage(source: ImageSource.gallery); if (image != null) { final imageBytes = await image.readAsBytes(); final updatedGame = await GameRepository.updateImage( Game( name: widget.game.name, versionRegex: _versionRegexController.text, lastPlayed: _lastPlayedController.text, rssFeedUrl: _rssFeedUrlController.text, actualVersion: widget.game.actualVersion, lastUpdated: widget.game.lastUpdated, imageData: imageBytes, ), ); widget.onGameUpdated(updatedGame); } } @override Widget build(BuildContext context) { final isUpToDate = widget.game.actualVersion == widget.game.lastPlayed; final hasImage = widget.game.imageData != null; return Card( child: Stack( children: [ if (hasImage) Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Opacity( opacity: 0.4, child: Image.memory(widget.game.imageData!, fit: BoxFit.cover), ), ), ), if (hasImage) Positioned.fill( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withAlpha(110), Colors.black.withAlpha(90), Colors.black.withAlpha(70), ], ), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 40, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: TextField( controller: _nameController, focusNode: _nameFocus, style: Theme.of(context).textTheme.titleLarge ?.copyWith(color: hasImage ? Colors.white : null), enabled: widget.isNameEditable, decoration: const InputDecoration.collapsed( hintText: 'New Game', ), onSubmitted: (_) => _nameFocus.unfocus(), ), ), Row( mainAxisSize: MainAxisSize.min, children: [ if (!widget.isNameEditable) IconButton( icon: const Icon(Icons.image), color: hasImage ? Colors.white : null, onPressed: _pickImage, ), if (widget.onDelete != null) IconButton( icon: Icon( Icons.delete, color: _deleteClickCount > 0 ? Colors.red : (hasImage ? Colors.white : null), ), onPressed: _handleDeleteClick, ), if (!widget.isNameEditable) RotationTransition( turns: _controller, child: IconButton( icon: Icon( Icons.refresh, color: hasImage ? Colors.white : null, ), onPressed: _isLoading ? null : _refreshVersion, ), ), ], ), ], ), ), const SizedBox(height: 16), Row( children: [ SizedBox( width: 200, child: Text( 'Version:', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: hasImage ? Colors.white : null, ), ), ), Text( widget.game.actualVersion.isEmpty ? 'Unknown' : widget.game.actualVersion, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isUpToDate ? Colors.green : Colors.red, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 12), Row( children: [ SizedBox( width: 200, child: Text( 'Last Updated:', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: hasImage ? Colors.white : null, ), ), ), Text( widget.game.lastUpdated.isEmpty ? 'Never' : DateTime.parse(widget.game.lastUpdated).toString(), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: hasImage ? Colors.white : null, ), ), ], ), const SizedBox(height: 12), Row( children: [ SizedBox( width: 200, child: Text( 'Last Played:', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: hasImage ? Colors.white : null, ), ), ), Text( widget.game.lastPlayed, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isUpToDate ? Colors.green : Colors.red, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 16), TextField( controller: _versionRegexController, focusNode: _versionRegexFocus, style: TextStyle(color: hasImage ? Colors.white : null), decoration: InputDecoration( labelText: 'Version Regex', labelStyle: TextStyle( color: hasImage ? Colors.white70 : null, ), enabledBorder: hasImage ? const UnderlineInputBorder( borderSide: BorderSide(color: Colors.white70), ) : null, ), onSubmitted: (_) => _versionRegexFocus.unfocus(), ), TextField( controller: _rssFeedUrlController, focusNode: _rssFeedUrlFocus, style: TextStyle(color: hasImage ? Colors.white : null), decoration: InputDecoration( labelText: 'RSS Feed URL', labelStyle: TextStyle( color: hasImage ? Colors.white70 : null, ), enabledBorder: hasImage ? const UnderlineInputBorder( borderSide: BorderSide(color: Colors.white70), ) : null, ), onSubmitted: (_) => _rssFeedUrlFocus.unfocus(), ), TextField( controller: _lastPlayedController, focusNode: _lastPlayedFocus, style: TextStyle(color: hasImage ? Colors.white : null), decoration: InputDecoration( labelText: 'Last Played', labelStyle: TextStyle( color: hasImage ? Colors.white70 : null, ), enabledBorder: hasImage ? const UnderlineInputBorder( borderSide: BorderSide(color: Colors.white70), ) : null, ), onSubmitted: (_) => _lastPlayedFocus.unfocus(), ), ], ), ), ], ), ); } }