18 Commits

17 changed files with 642 additions and 237 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,32 +31,34 @@ 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) {
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;
lastUpdated = parseRfc822Date(page.pubDate!).toIso8601String();
break;
}
}
lastUpdated =
parseRfc822Date(
response.headers['last-modified'] ?? '',
).toIso8601String();
}
}
@@ -86,12 +90,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

@@ -32,9 +32,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 +53,7 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
late List<Game> games = [];
late Map<String, Game> games = {};
@override
void initState() {
@@ -63,6 +63,14 @@ class _MyHomePageState extends State<MyHomePage> {
Future<void> _refreshGames() async {
final games = await GameRepository.getAll();
games.forEach((key, game) {
game.updateActualVersion().then((_) {
GameRepository.upsert(game);
setState(() {
games[game.name] = game;
});
});
});
setState(() {
this.games = games;
});
@@ -80,44 +88,47 @@ class _MyHomePageState extends State<MyHomePage> {
),
body: RefreshIndicator(
onRefresh: _refreshGames,
child: GridView.builder(
child: SingleChildScrollView(
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],
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
...games.values.map(
(game) => SizedBox(
width: (MediaQuery.of(context).size.width - 24) / 2,
child: GameCard(
game: game,
onGameUpdated: (game) async {
game = await GameRepository.upsert(game);
setState(() {
games[index] = game;
games[game.name] = game;
});
},
onDelete: () async {
await GameRepository.delete(games[index]);
await GameRepository.delete(game);
setState(() {
games.removeAt(index);
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;
try {
await updatedGame.updateActualVersion();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
widget.onGameUpdated(updatedGame);
_controller.stop();
@@ -72,52 +149,112 @@ 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: 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),
],
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextField(
controller: _nameController,
style: Theme.of(context).textTheme.titleLarge,
focusNode: _nameFocus,
style: Theme.of(context).textTheme.titleLarge
?.copyWith(color: hasImage ? Colors.white : null),
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,
)),
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 : null,
color:
_deleteClickCount > 0
? Colors.red
: (hasImage ? Colors.white : null),
),
onPressed: _handleDeleteClick,
),
if (!widget.isNameEditable)
RotationTransition(
turns: _controller,
child: IconButton(
icon: const Icon(Icons.refresh),
icon: Icon(
Icons.refresh,
color: hasImage ? Colors.white : null,
),
onPressed: _isLoading ? null : _refreshVersion,
),
),
@@ -125,15 +262,23 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
),
],
),
const SizedBox(height: 8),
),
const SizedBox(height: 16),
Row(
children: [
SizedBox(
width: 120,
child: Text('Version:', style: Theme.of(context).textTheme.bodyLarge),
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,
widget.game.actualVersion.isEmpty
? 'Unknown'
: widget.game.actualVersion,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: isUpToDate ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
@@ -141,23 +286,39 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
),
],
),
const SizedBox(height: 12),
Row(
children: [
SizedBox(
width: 120,
child: Text('Last Updated:', style: Theme.of(context).textTheme.bodyLarge),
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,
widget.game.lastUpdated.isEmpty
? 'Never'
: DateTime.parse(widget.game.lastUpdated).toString(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: hasImage ? Colors.white : null,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
SizedBox(
width: 120,
child: Text('Last Played:', style: Theme.of(context).textTheme.bodyLarge),
width: 200,
child: Text(
'Last Played:',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: hasImage ? Colors.white : null,
),
),
),
Text(
widget.game.lastPlayed,
@@ -168,46 +329,68 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
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,
)),
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: 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,
)),
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: 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,
)),
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(),
),
],
),
),
],
),
);
}
}

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) {

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