397 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			397 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| 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<GameCard> createState() => _GameCardState();
 | |
| }
 | |
| 
 | |
| class _GameCardState extends State<GameCard>
 | |
|     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<void> _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<void> _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: 120,
 | |
|                       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: 120,
 | |
|                       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: 120,
 | |
|                       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(),
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |