From e5fc67ef431e9dc8a19c2514aad8d68ed1fe6a23 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Sat, 22 Feb 2025 16:58:03 +0100 Subject: [PATCH] Add background images for games --- lib/db.dart | 3 +- lib/game.dart | 15 +- lib/main.dart | 9 +- lib/widgets/game_card.dart | 372 ++++++++++++------ lib/widgets/new_game_card.dart | 7 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 135 ++++++- pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 414 insertions(+), 139 deletions(-) diff --git a/lib/db.dart b/lib/db.dart index 5922c89..6da29cc 100644 --- a/lib/db.dart +++ b/lib/db.dart @@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS games ( last_played TEXT NOT NULL, rss_feed_url TEXT NOT NULL, version_regex TEXT NOT NULL, - last_updated TEXT NOT NULL + last_updated TEXT NOT NULL, + image_data BLOB ); CREATE INDEX IF NOT EXISTS idx_games_name ON games (name); CREATE UNIQUE INDEX IF NOT EXISTS idx_games_name_unique ON games (name); diff --git a/lib/game.dart b/lib/game.dart index baebdc2..0d2566b 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -1,6 +1,7 @@ import 'package:gamer_updater/db.dart'; import 'package:gamer_updater/utils.dart'; import 'package:http/http.dart' as http; +import 'dart:typed_data'; class Game { final String name; @@ -10,6 +11,7 @@ class Game { String actualVersion; String lastUpdated; final String rssFeedUrl; + final Uint8List? imageData; Game({ required this.name, @@ -18,6 +20,7 @@ class Game { this.actualVersion = '', required this.rssFeedUrl, this.lastUpdated = '', + this.imageData, }) : _internalVersionRegex = RegExp(versionRegex); factory Game.fromMap(Map map) { @@ -28,6 +31,7 @@ class Game { rssFeedUrl: map['rss_feed_url'], actualVersion: map['actual_version'], lastUpdated: map['last_updated'], + imageData: map['image_data'], ); } @@ -86,10 +90,19 @@ last_updated = excluded.last_updated return game; } + static Future updateImage(Game game) async { + final db = DB.db; + await db.rawUpdate('UPDATE games SET image_data = ? WHERE name = ?', [ + game.imageData, + game.name, + ]); + return game; + } + static Future> getAll() async { final db = DB.db; final games = await db.rawQuery( - 'SELECT name, actual_version, last_played, rss_feed_url, version_regex, last_updated FROM games', + 'SELECT name, actual_version, last_played, rss_feed_url, version_regex, last_updated, image_data FROM games', ); return games .map((e) => Game.fromMap(e)) diff --git a/lib/main.dart b/lib/main.dart index cbd5478..0d114d2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -82,7 +82,8 @@ class _MyHomePageState extends State { onRefresh: _refreshGames, child: LayoutBuilder( builder: (context, constraints) { - final cardWidth = constraints.maxWidth / 4; + final cardWidth = constraints.maxWidth / 2; + final cardHeight = (cardWidth * 215) / 400; // Maintain aspect ratio return SingleChildScrollView( padding: const EdgeInsets.all(8), child: Wrap( @@ -91,7 +92,8 @@ class _MyHomePageState extends State { children: [ ...games.values.map( (game) => SizedBox( - width: cardWidth - 10, // Subtract spacing to fit 4 cards + width: cardWidth - 10, // Subtract spacing to fit 3 cards + height: cardHeight - 10, child: GameCard( game: game, onGameUpdated: (game) async { @@ -110,7 +112,8 @@ class _MyHomePageState extends State { ), ), SizedBox( - width: cardWidth - 10, // Subtract spacing to fit 4 cards + width: cardWidth - 10, // Subtract spacing to fit 3 cards + height: cardHeight - 10, child: NewGameCard( onGameCreated: (game) async { game = await GameRepository.upsert(game); diff --git a/lib/widgets/game_card.dart b/lib/widgets/game_card.dart index 2892ddc..af20032 100644 --- a/lib/widgets/game_card.dart +++ b/lib/widgets/game_card.dart @@ -1,5 +1,6 @@ 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; @@ -57,19 +58,28 @@ class _GameCardState extends State void _setupFocusListeners() { void updateGame() { - 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, - ), - ); + 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()))); + } } } @@ -139,134 +149,240 @@ class _GameCardState extends State }); } + 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: Padding( - padding: const EdgeInsets.all(16.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, - enabled: widget.isNameEditable, - decoration: const InputDecoration.collapsed( - hintText: 'New Game', - ), - onSubmitted: (_) => _nameFocus.unfocus(), - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.onDelete != null) - IconButton( - icon: Icon( - Icons.delete, - color: _deleteClickCount > 0 ? Colors.red : null, - ), - onPressed: _handleDeleteClick, - ), - if (!widget.isNameEditable) - RotationTransition( - turns: _controller, - child: IconButton( - icon: const Icon(Icons.refresh), - onPressed: _isLoading ? null : _refreshVersion, - ), - ), - ], - ), - ], + child: Stack( + children: [ + if (hasImage) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.memory(widget.game.imageData!, fit: BoxFit.cover), ), ), - const SizedBox(height: 8), - Row( - children: [ - SizedBox( - width: 120, - child: Text( - 'Version:', - style: Theme.of(context).textTheme.bodyLarge, + if (hasImage) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.7), + Colors.black.withOpacity(0.9), + ], ), ), - 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, + ), + ), + Padding( + padding: const EdgeInsets.all(16.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: 8), + 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, + ), + ), + ], + ), + 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, + ), + ), + ], + ), + 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 Divider(), + 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(), + ), ], ), - 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: _versionRegexController, - focusNode: _versionRegexFocus, - decoration: const InputDecoration(labelText: 'Version Regex'), - onSubmitted: (_) => _versionRegexFocus.unfocus(), - ), - TextField( - controller: _rssFeedUrlController, - focusNode: _rssFeedUrlFocus, - decoration: const InputDecoration(labelText: 'RSS Feed URL'), - onSubmitted: (_) => _rssFeedUrlFocus.unfocus(), - ), - TextField( - controller: _lastPlayedController, - focusNode: _lastPlayedFocus, - decoration: const InputDecoration(labelText: 'Last Played'), - onSubmitted: (_) => _lastPlayedFocus.unfocus(), - ), - ], - ), + ), + ], ), ); } diff --git a/lib/widgets/new_game_card.dart b/lib/widgets/new_game_card.dart index 978b0d2..9910925 100644 --- a/lib/widgets/new_game_card.dart +++ b/lib/widgets/new_game_card.dart @@ -5,10 +5,7 @@ import 'package:gamer_updater/widgets/game_card.dart'; class NewGameCard extends StatelessWidget { final Function(Game) onGameCreated; - const NewGameCard({ - super.key, - required this.onGameCreated, - }); + const NewGameCard({super.key, required this.onGameCreated}); @override Widget build(BuildContext context) { @@ -25,4 +22,4 @@ class NewGameCard extends StatelessWidget { onGameUpdated: onGameCreated, ); } -} \ No newline at end of file +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..64a0ece 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..2db3c22 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..14b5f7c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import file_selector_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index bae0042..0d46ee3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" cupertino_icons: dependency: "direct main" description: @@ -65,6 +73,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" flutter: dependency: "direct main" description: flutter @@ -78,11 +118,24 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + url: "https://pub.dev" + source: hosted + version: "2.0.24" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" http: dependency: "direct main" description: @@ -99,6 +152,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a" + url: "https://pub.dev" + source: hosted + version: "0.8.12+21" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" leak_tracker: dependency: transitive description: @@ -155,6 +272,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -163,6 +288,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -282,4 +415,4 @@ packages: version: "1.1.0" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index ef3b514..d1ce213 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: cupertino_icons: ^1.0.8 sqflite_common_ffi: ^2.3.5 http: ^1.3.0 + image_picker: ^1.0.7 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..77ab7a0 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..a423a02 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST