diff --git a/lib/game.dart b/lib/game.dart index 17eae39..7ca3cd7 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -93,6 +93,11 @@ last_updated = excluded.last_updated ); return games.map((e) => Game.fromMap(e)).toList(); } + + static Future delete(Game game) async { + final db = DB.db; + await db.rawDelete('DELETE FROM games WHERE name = ?', [game.name]); + } } //CREATE TABLE IF NOT EXISTS games ( diff --git a/lib/main.dart b/lib/main.dart index 4ce1f90..619e430 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:gamer_updater/db.dart'; import 'package:gamer_updater/game.dart'; +import 'package:gamer_updater/widgets/new_game_card.dart'; void main() async { await DB.init(); @@ -86,8 +87,18 @@ class _MyHomePageState extends State { crossAxisSpacing: 4, mainAxisSpacing: 4, ), - itemCount: games.length, + itemCount: games.length + 1, // +1 for the new game card itemBuilder: (context, index) { + if (index == games.length) { + return NewGameCard( + onGameCreated: (game) async { + game = await GameRepository.upsert(game); + setState(() { + games.add(game); + }); + }, + ); + } return GameCard( game: games[index], onGameUpdated: (game) async { @@ -96,6 +107,12 @@ class _MyHomePageState extends State { games[index] = game; }); }, + onDelete: () async { + await GameRepository.delete(games[index]); + setState(() { + games.removeAt(index); + }); + }, ); }, ), @@ -107,8 +124,14 @@ class _MyHomePageState extends State { class GameCard extends StatefulWidget { final Game game; final Function(Game) onGameUpdated; + final VoidCallback onDelete; - const GameCard({super.key, required this.game, required this.onGameUpdated}); + const GameCard({ + super.key, + required this.game, + required this.onGameUpdated, + required this.onDelete, + }); @override State createState() => _GameCardState(); diff --git a/lib/widgets/game_card.dart b/lib/widgets/game_card.dart new file mode 100644 index 0000000..9562bb2 --- /dev/null +++ b/lib/widgets/game_card.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:gamer_updater/game.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; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + ); + _nameController = TextEditingController(text: widget.game.name); + } + + @override + void dispose() { + _controller.dispose(); + _nameController.dispose(); + super.dispose(); + } + + Future _refreshVersion() async { + setState(() => _isLoading = true); + _controller.repeat(); + + final updatedGame = widget.game; + await updatedGame.updateActualVersion(); + 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); + } + }); + } + + @override + Widget build(BuildContext context) { + final isUpToDate = widget.game.actualVersion == widget.game.lastPlayed; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: widget.isNameEditable + ? TextField( + controller: _nameController, + style: Theme.of(context).textTheme.titleLarge, + decoration: const InputDecoration( + labelText: 'Game Name', + border: OutlineInputBorder(), + ), + onChanged: (value) => widget.onGameUpdated(Game( + name: value, + versionRegex: widget.game.versionRegex, + lastPlayed: widget.game.lastPlayed, + rssFeedUrl: widget.game.rssFeedUrl, + actualVersion: widget.game.actualVersion, + lastUpdated: widget.game.lastUpdated, + )), + ) + : Text( + widget.game.name, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + RotationTransition( + turns: _controller, + child: IconButton( + icon: const Icon(Icons.refresh), + onPressed: _isLoading ? null : _refreshVersion, + ), + ), + if (widget.onDelete != null) + IconButton( + icon: Icon( + Icons.delete, + color: _deleteClickCount > 0 ? Colors.red : null, + ), + onPressed: _handleDeleteClick, + ), + ], + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + SizedBox( + width: 120, + child: Text('Version:', style: Theme.of(context).textTheme.bodyLarge), + ), + 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, + ), + ), + ], + ), + Row( + children: [ + SizedBox( + width: 120, + child: Text('Last Updated:', style: Theme.of(context).textTheme.bodyLarge), + ), + Text( + widget.game.lastUpdated.isEmpty ? 'Never' : DateTime.parse(widget.game.lastUpdated).toString(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + Row( + children: [ + SizedBox( + width: 120, + child: Text('Last Played:', style: Theme.of(context).textTheme.bodyLarge), + ), + Text( + widget.game.lastPlayed, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: isUpToDate ? Colors.green : Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Divider(), + TextField( + controller: TextEditingController(text: widget.game.versionRegex), + decoration: const InputDecoration(labelText: 'Version Regex'), + onChanged: (value) => widget.onGameUpdated(Game( + name: widget.isNameEditable ? _nameController.text : widget.game.name, + versionRegex: value, + lastPlayed: widget.game.lastPlayed, + rssFeedUrl: widget.game.rssFeedUrl, + actualVersion: widget.game.actualVersion, + lastUpdated: widget.game.lastUpdated, + )), + ), + TextField( + controller: TextEditingController(text: widget.game.rssFeedUrl), + decoration: const InputDecoration(labelText: 'RSS Feed URL'), + onChanged: (value) => widget.onGameUpdated(Game( + name: widget.isNameEditable ? _nameController.text : widget.game.name, + versionRegex: widget.game.versionRegex, + lastPlayed: widget.game.lastPlayed, + rssFeedUrl: value, + actualVersion: widget.game.actualVersion, + lastUpdated: widget.game.lastUpdated, + )), + ), + TextField( + controller: TextEditingController(text: widget.game.lastPlayed), + decoration: const InputDecoration(labelText: 'Last Played'), + onChanged: (value) => widget.onGameUpdated(Game( + name: widget.isNameEditable ? _nameController.text : widget.game.name, + versionRegex: widget.game.versionRegex, + lastPlayed: value, + rssFeedUrl: widget.game.rssFeedUrl, + actualVersion: widget.game.actualVersion, + lastUpdated: widget.game.lastUpdated, + )), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/new_game_card.dart b/lib/widgets/new_game_card.dart new file mode 100644 index 0000000..978b0d2 --- /dev/null +++ b/lib/widgets/new_game_card.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:gamer_updater/game.dart'; +import 'package:gamer_updater/widgets/game_card.dart'; + +class NewGameCard extends StatelessWidget { + final Function(Game) onGameCreated; + + const NewGameCard({ + super.key, + required this.onGameCreated, + }); + + @override + Widget build(BuildContext context) { + return GameCard( + game: Game( + name: '', + versionRegex: '', + lastPlayed: '', + rssFeedUrl: '', + actualVersion: '', + lastUpdated: '', + ), + isNameEditable: true, + onGameUpdated: onGameCreated, + ); + } +} \ No newline at end of file