diff --git a/lib/game.dart b/lib/game.dart index 4ea6f19..17eae39 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -60,7 +60,7 @@ class Game { class GameRepository { static Future upsert(Game game) async { - final db = await DB.db; + final db = DB.db; await db.rawInsert( ''' INSERT INTO games diff --git a/lib/main.dart b/lib/main.dart index 268e411..4ce1f90 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,17 +17,23 @@ class MyApp extends StatelessWidget { title: 'Gamer Updater', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, ), darkTheme: ThemeData( brightness: Brightness.dark, useMaterial3: true, scaffoldBackgroundColor: Colors.black, - colorSchemeSeed: Colors.black, - highlightColor: Colors.deepPurple, - textTheme: TextTheme( - bodyLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), - bodyMedium: TextStyle(fontSize: 20), - bodySmall: TextStyle(fontSize: 16), + colorSchemeSeed: Colors.deepPurple, + cardTheme: CardTheme( + color: Colors.grey[900], + elevation: 4, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + bodyLarge: TextStyle(fontSize: 18), + bodyMedium: TextStyle(fontSize: 16), + bodySmall: TextStyle(fontSize: 14), ), ), themeMode: ThemeMode.system, @@ -50,14 +56,13 @@ class _MyHomePageState extends State { @override void initState() { super.initState(); - GameRepository.getAll().then((games) { - setState(() { - this.games = games; - }); - for (var e in games) { - e.updateActualVersion(); - GameRepository.upsert(e); - } + _refreshGames(); + } + + Future _refreshGames() async { + final games = await GameRepository.getAll(); + setState(() { + this.games = games; }); } @@ -67,13 +72,209 @@ class _MyHomePageState extends State { appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), + actions: [ + IconButton(icon: const Icon(Icons.refresh), onPressed: _refreshGames), + ], ), - body: Center( + body: RefreshIndicator( + onRefresh: _refreshGames, + child: GridView.builder( + padding: const EdgeInsets.all(8), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 1.3, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + itemCount: games.length, + itemBuilder: (context, index) { + return GameCard( + game: games[index], + onGameUpdated: (game) async { + game = await GameRepository.upsert(game); + setState(() { + games[index] = game; + }); + }, + ); + }, + ), + ), + ); + } +} + +class GameCard extends StatefulWidget { + final Game game; + final Function(Game) onGameUpdated; + + const GameCard({super.key, required this.game, required this.onGameUpdated}); + + @override + State createState() => _GameCardState(); +} + +class _GameCardState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + ); + } + + @override + void dispose() { + _controller.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); + } + + @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( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(games.map((e) => e.name).join('\n')), - Text(games.map((e) => e.actualVersion).join('\n')), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.game.name, + style: Theme.of(context).textTheme.titleLarge, + ), + RotationTransition( + turns: _controller, + child: IconButton( + icon: const Icon(Icons.refresh), + onPressed: _isLoading ? null : _refreshVersion, + ), + ), + ], + ), + 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.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.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.game.name, + versionRegex: widget.game.versionRegex, + lastPlayed: value, + rssFeedUrl: widget.game.rssFeedUrl, + actualVersion: widget.game.actualVersion, + lastUpdated: widget.game.lastUpdated, + ), + ), + ), ], ), ),