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(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( 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.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, home: const MyHomePage(title: 'Gamer Updater'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { late List games = []; @override void initState() { super.initState(); _refreshGames(); } Future _refreshGames() async { final games = await GameRepository.getAll(); setState(() { this.games = games; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _refreshGames), ], ), 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 + 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 { game = await GameRepository.upsert(game); setState(() { games[index] = game; }); }, onDelete: () async { await GameRepository.delete(games[index]); setState(() { games.removeAt(index); }); }, ); }, ), ), ); } } class GameCard extends StatefulWidget { final Game game; final Function(Game) onGameUpdated; final VoidCallback onDelete; const GameCard({ super.key, required this.game, required this.onGameUpdated, required this.onDelete, }); @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( 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, ), ), ), ], ), ), ); } }