19 Commits

Author SHA1 Message Date
336cb87a06 Implement some sort of filtering 2025-02-26 21:02:07 +01:00
795060a05b Fix grid a fucking gain 2025-02-22 18:13:09 +01:00
bbd3583939 Sort games by name 2025-02-22 18:07:46 +01:00
1c1ac3385b Update README 2025-02-22 17:38:27 +01:00
eeceb706d6 Tag apk too 2025-02-22 17:23:03 +01:00
165efcd1a3 Fuck with styles a little 2025-02-22 17:22:23 +01:00
509849db5b Add version to zip file 2025-02-22 17:20:10 +01:00
82f8748177 Fix refresh all button 2025-02-22 17:17:31 +01:00
8fd0511242 Add release script 2025-02-22 17:14:56 +01:00
4f1a947d2b Fix the god damn grid 2025-02-22 17:12:01 +01:00
e5fc67ef43 Add background images for games 2025-02-22 16:58:03 +01:00
0a40e5bbcf Improve error handling and parsing a little 2025-02-22 16:13:18 +01:00
406be305e2 Fix up the grid agin 2025-02-22 15:51:53 +01:00
edb7dbfe05 Trim version 2025-02-22 15:45:55 +01:00
fef5f199c3 Rework data structure to map from list 2025-02-22 15:31:23 +01:00
c94a8f8926 Get rid of that stinky ass weird ass grid sliver 2025-02-22 15:22:49 +01:00
362dea6b08 Design polish 2025-02-22 15:19:43 +01:00
771cf90349 Add a million focus nodes for good measure 2025-02-22 15:15:09 +01:00
b76b51ff34 Don't refresh new games 2025-02-22 15:09:07 +01:00
17 changed files with 714 additions and 240 deletions

View File

@@ -1,16 +1,53 @@
# gamer_updater
# Gamer Updater
A new Flutter project.
A Flutter application for tracking game version updates via RSS feeds.<br>
The idea is to track updates to games you might have previously played<br>
To simply know how much you might be missing and whether you would have new content to look forward to
## Getting Started
![](git_static/screenshot.jpg)
This project is a starting point for a Flutter application.
## Features
A few resources to get you started if this is your first Flutter project:
- Game version tracking
- Automatic version checking through RSS feeds
- Last played date tracking
- Custom version regex patterns
- Thumbnails!
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
## Setup
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
1. Clone repository
2. Install Flutter
3. Run `flutter pub get`
4. Run `flutter run`
## Usage
Add games by providing:
- Name
- RSS feed URL
- Version regex pattern
- Optional game image
## Example
If we wanted to track the versions of Rimworld we would enter the rss feed as:
https://store.steampowered.com/feeds/news/app/294100/?cc=HR&l=english
And the version regex pattern as:
`Update (\d+\.\d+\.\d+)`
Seeing as their posts usually follow this convention:
Update 1.5.4104 released
...
Update 1.4.3704 released
In theory the rss link can be any link at all and the version regex can be anything at all<br>
The regex is simply ran on the entirety of the contents of the get request to the rss link<br>
Which means we can also simply search html<br>
But it is not as reliable as the rss because of all the additional shit
Currently the thumbnails must be manually added
They can also be any image at all but the cards are designed to fit the thumbnails from the game update pages:
![](git_static/thumbnails.png)

BIN
git_static/screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

BIN
git_static/thumbnails.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

View File

@@ -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);

View File

@@ -1,7 +1,7 @@
import 'package:gamer_updater/db.dart';
import 'package:gamer_updater/utils.dart';
import 'package:http/http.dart' as http;
import 'package:dart_rss/dart_rss.dart';
import 'dart:typed_data';
class Game {
final String name;
@@ -11,6 +11,7 @@ class Game {
String actualVersion;
String lastUpdated;
final String rssFeedUrl;
final Uint8List? imageData;
Game({
required this.name,
@@ -19,6 +20,7 @@ class Game {
this.actualVersion = '',
required this.rssFeedUrl,
this.lastUpdated = '',
this.imageData,
}) : _internalVersionRegex = RegExp(versionRegex);
factory Game.fromMap(Map<String, dynamic> map) {
@@ -29,31 +31,40 @@ class Game {
rssFeedUrl: map['rss_feed_url'],
actualVersion: map['actual_version'],
lastUpdated: map['last_updated'],
imageData: map['image_data'],
);
}
Future<void> updateActualVersion() async {
if (rssFeedUrl.isEmpty) {
throw Exception('No rss feed url for $name');
}
final response = await http.get(Uri.parse(rssFeedUrl));
final document = RssFeed.parse(response.body);
final pages = document.items;
pages.sort((a, b) {
var lhs = parseRfc822Date(a.pubDate!);
var rhs = parseRfc822Date(b.pubDate!);
return rhs.compareTo(lhs);
});
final versions =
pages
.map((e) => _internalVersionRegex.firstMatch(e.title!)?.group(1))
.toList();
for (int i = 0; i < versions.length; i++) {
final version = versions[i];
final page = pages[i];
if (version != null) {
actualVersion = version;
lastUpdated = parseRfc822Date(page.pubDate!).toIso8601String();
break;
}
if (response.statusCode != 200) {
throw Exception(
'Failed to update actual version for $name, rss responded with ${response.statusCode}',
);
}
final body = response.body;
final match = _internalVersionRegex.firstMatch(body);
if (match == null || match.groupCount == 0) {
throw Exception('No version found for $name');
}
final version = match.group(1);
if (version == null) {
throw Exception('No version found for $name');
}
actualVersion = version;
try {
// Some sites use weird ass dogshit fucking formats
// We cannot really reliably parse every single one of them
// So - fuck it
lastUpdated =
parseRfc822Date(
response.headers['last-modified'] ?? '',
).toIso8601String();
} catch (e) {
lastUpdated = DateTime.now().toIso8601String();
}
}
}
@@ -86,12 +97,23 @@ last_updated = excluded.last_updated
return game;
}
static Future<List<Game>> getAll() async {
static Future<Game> 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<Map<String, Game>> 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 ORDER BY name',
);
return games.map((e) => Game.fromMap(e)).toList();
return games
.map((e) => Game.fromMap(e))
.fold<Map<String, Game>>({}, (map, game) => {...map, game.name: game});
}
static Future<void> delete(Game game) async {

View File

@@ -3,6 +3,7 @@ import 'package:gamer_updater/db.dart';
import 'package:gamer_updater/game.dart';
import 'package:gamer_updater/widgets/new_game_card.dart';
import 'package:gamer_updater/widgets/game_card.dart';
import 'dart:async';
void main() async {
await DB.init();
@@ -32,9 +33,9 @@ class MyApp extends StatelessWidget {
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),
titleLarge: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
bodyLarge: TextStyle(fontSize: 20),
bodyMedium: TextStyle(fontSize: 20),
bodySmall: TextStyle(fontSize: 14),
),
),
@@ -53,7 +54,10 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
late List<Game> games = [];
late Map<String, Game> games = {};
String searchQuery = "";
final TextEditingController _searchController = TextEditingController();
Timer? _debounce;
@override
void initState() {
@@ -61,13 +65,46 @@ class _MyHomePageState extends State<MyHomePage> {
_refreshGames();
}
@override
void dispose() {
_searchController.dispose();
_debounce?.cancel();
super.dispose();
}
Future<void> _refreshGames() async {
final games = await GameRepository.getAll();
games.forEach((key, game) {
game.updateActualVersion().then((_) {
GameRepository.upsert(game);
setState(() {
this.games[game.name] = game;
});
});
});
setState(() {
this.games = games;
});
}
List<Game> get filteredGames {
if (searchQuery.isEmpty) {
return games.values.toList();
}
return games.values.where((game) =>
game.name.toLowerCase().contains(searchQuery.toLowerCase())
).toList();
}
void _onSearchChanged(String value) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 100), () {
setState(() {
searchQuery = value;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -78,45 +115,81 @@ class _MyHomePageState extends State<MyHomePage> {
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,
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search games...',
prefixIcon: const Icon(Icons.search),
suffixIcon: searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
searchQuery = "";
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
onChanged: _onSearchChanged,
),
),
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);
});
},
);
},
),
Expanded(
child: RefreshIndicator(
onRefresh: _refreshGames,
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
...filteredGames.map(
(game) => SizedBox(
key: ValueKey(game.name),
width: (MediaQuery.of(context).size.width - 24) / 2,
child: GameCard(
key: ValueKey('card_${game.name}'),
game: game,
onGameUpdated: (game) async {
game = await GameRepository.upsert(game);
setState(() {
games[game.name] = game;
});
},
onDelete: () async {
await GameRepository.delete(game);
setState(() {
games.remove(game.name);
});
},
),
),
),
SizedBox(
width: (MediaQuery.of(context).size.width - 24) / 2,
child: NewGameCard(
onGameCreated: (game) async {
game = await GameRepository.upsert(game);
setState(() {
games[game.name] = game;
});
},
),
),
],
),
),
),
),
],
),
);
}

View File

@@ -27,21 +27,8 @@ DateTime parseRfc822Date(String date) {
final minute = int.parse(timeParts[1]);
final second = int.parse(timeParts[2]);
// Handle the timezone offset
final timezone = parts[5];
final isNegative = timezone.startsWith('-');
final tzHours = int.parse(timezone.substring(1, 3));
final tzMinutes = int.parse(timezone.substring(3, 5));
// Create the DateTime object
DateTime dateTime = DateTime(year, month, day, hour, minute, second);
// Adjust for timezone
if (isNegative) {
dateTime = dateTime.subtract(Duration(hours: tzHours, minutes: tzMinutes));
} else {
dateTime = dateTime.add(Duration(hours: tzHours, minutes: tzMinutes));
}
return dateTime;
}

View File

@@ -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;
@@ -19,11 +20,19 @@ class GameCard extends StatefulWidget {
State<GameCard> createState() => _GameCardState();
}
class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin {
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() {
@@ -33,12 +42,72 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
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();
}
@@ -47,7 +116,15 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
_controller.repeat();
final updatedGame = widget.game;
await updatedGame.updateActualVersion();
try {
await updatedGame.updateActualVersion();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
widget.onGameUpdated(updatedGame);
_controller.stop();
@@ -72,142 +149,248 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
});
}
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: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextField(
controller: _nameController,
style: Theme.of(context).textTheme.titleLarge,
enabled: widget.isNameEditable,
decoration: const InputDecoration.collapsed(
hintText: 'New Game',
),
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,
)),
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),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.onDelete != null)
IconButton(
icon: Icon(
Icons.delete,
color: _deleteClickCount > 0 ? Colors.red : null,
),
),
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(),
),
onPressed: _handleDeleteClick,
),
RotationTransition(
turns: _controller,
child: IconButton(
icon: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _refreshVersion,
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: 200,
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: 8),
Row(
children: [
SizedBox(
width: 120,
child: Text('Version:', style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 12),
Row(
children: [
SizedBox(
width: 200,
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,
),
),
],
),
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: 200,
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(),
),
],
),
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,
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(),
),
],
),
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,
)),
),
],
),
),
],
),
);
}
}
}

View File

@@ -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,
);
}
}
}

View File

@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
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);
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import file_selector_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
}

View File

@@ -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:
@@ -49,14 +57,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dart_rss:
dependency: "direct main"
description:
name: dart_rss
sha256: "73539d4b7153b47beef8b51763ca55dcb6fc0bb412b29e0f5e74e93fabfd1ac6"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
fake_async:
dependency: transitive
description:
@@ -73,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
@@ -86,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:
@@ -107,14 +152,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: transitive
image_picker:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
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:
@@ -171,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:
@@ -179,14 +288,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
petitparser:
plugin_platform_interface:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
@@ -304,14 +413,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.24.0"

View File

@@ -36,7 +36,7 @@ dependencies:
cupertino_icons: ^1.0.8
sqflite_common_ffi: ^2.3.5
http: ^1.3.0
dart_rss: ^3.0.3
image_picker: ^1.0.7
dev_dependencies:
flutter_test:

62
release.sh Normal file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
# Determine the tag
echo "Figuring out the tag..."
TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
if [ -z "$TAG" ]; then
# Get the latest tag
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
# Increment the patch version
IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_TAG"
VERSION_PARTS[2]=$((VERSION_PARTS[2]+1))
TAG="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}"
# Create a new tag
git tag $TAG
git push origin $TAG
fi
echo "Tag: $TAG"
# Build the application
echo "Building the thing..."
flutter build windows --release
flutter build apk --release
echo "Creating a release..."
TOKEN="$GITEA_API_KEY"
GITEA="https://git.site.quack-lab.dev"
REPO="dave/flutter-gamer-updater"
ZIP="gamer-updater-${TAG}.zip"
APK="gamer-updater-${TAG}.apk"
# Create a release
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "'"$TAG"'",
"name": "'"$TAG"'",
"draft": false,
"prerelease": false
}' \
$GITEA/api/v1/repos/$REPO/releases)
# Extract the release ID
echo $RELEASE_RESPONSE
RELEASE_ID=$(echo $RELEASE_RESPONSE | awk -F'"id":' '{print $2+0; exit}')
echo "Release ID: $RELEASE_ID"
echo "Uploading the things..."
WINRELEASE="./build/windows/x64/runner/Release/"
7z a $WINRELEASE/$ZIP $WINRELEASE/*
curl -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@$WINRELEASE/$ZIP" \
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=$ZIP"
rm $WINRELEASE/$ZIP
ANDROIDRELEASE="./build/app/outputs/flutter-apk/"
mv $ANDROIDRELEASE/app-release.apk $ANDROIDRELEASE/$APK
curl -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@$ANDROIDRELEASE/$APK" \
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=$APK"

View File

@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST