Compare commits
20 Commits
1ec8fa1f0d
...
v1.3.0
Author | SHA1 | Date | |
---|---|---|---|
ba8b669399 | |||
336cb87a06 | |||
795060a05b | |||
bbd3583939 | |||
1c1ac3385b | |||
eeceb706d6 | |||
165efcd1a3 | |||
509849db5b | |||
82f8748177 | |||
8fd0511242 | |||
4f1a947d2b | |||
e5fc67ef43 | |||
0a40e5bbcf | |||
406be305e2 | |||
edb7dbfe05 | |||
fef5f199c3 | |||
c94a8f8926 | |||
362dea6b08 | |||
771cf90349 | |||
b76b51ff34 |
57
README.md
57
README.md
@@ -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
|

|
||||||
|
|
||||||
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)
|
## Setup
|
||||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
1. Clone repository
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
2. Install Flutter
|
||||||
samples, guidance on mobile development, and a full API reference.
|
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:
|
||||||
|

|
||||||
|
BIN
git_static/screenshot.jpg
Normal file
BIN
git_static/screenshot.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 299 KiB |
BIN
git_static/thumbnails.png
Normal file
BIN
git_static/thumbnails.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 662 KiB |
@@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS games (
|
|||||||
last_played TEXT NOT NULL,
|
last_played TEXT NOT NULL,
|
||||||
rss_feed_url TEXT NOT NULL,
|
rss_feed_url TEXT NOT NULL,
|
||||||
version_regex 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 INDEX IF NOT EXISTS idx_games_name ON games (name);
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_games_name_unique ON games (name);
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_games_name_unique ON games (name);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import 'package:gamer_updater/db.dart';
|
import 'package:gamer_updater/db.dart';
|
||||||
import 'package:gamer_updater/utils.dart';
|
import 'package:gamer_updater/utils.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:dart_rss/dart_rss.dart';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
class Game {
|
class Game {
|
||||||
final String name;
|
final String name;
|
||||||
@@ -11,6 +11,7 @@ class Game {
|
|||||||
String actualVersion;
|
String actualVersion;
|
||||||
String lastUpdated;
|
String lastUpdated;
|
||||||
final String rssFeedUrl;
|
final String rssFeedUrl;
|
||||||
|
final Uint8List? imageData;
|
||||||
|
|
||||||
Game({
|
Game({
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -19,6 +20,7 @@ class Game {
|
|||||||
this.actualVersion = '',
|
this.actualVersion = '',
|
||||||
required this.rssFeedUrl,
|
required this.rssFeedUrl,
|
||||||
this.lastUpdated = '',
|
this.lastUpdated = '',
|
||||||
|
this.imageData,
|
||||||
}) : _internalVersionRegex = RegExp(versionRegex);
|
}) : _internalVersionRegex = RegExp(versionRegex);
|
||||||
|
|
||||||
factory Game.fromMap(Map<String, dynamic> map) {
|
factory Game.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -29,31 +31,40 @@ class Game {
|
|||||||
rssFeedUrl: map['rss_feed_url'],
|
rssFeedUrl: map['rss_feed_url'],
|
||||||
actualVersion: map['actual_version'],
|
actualVersion: map['actual_version'],
|
||||||
lastUpdated: map['last_updated'],
|
lastUpdated: map['last_updated'],
|
||||||
|
imageData: map['image_data'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateActualVersion() async {
|
Future<void> updateActualVersion() async {
|
||||||
final response = await http.get(Uri.parse(rssFeedUrl));
|
if (rssFeedUrl.isEmpty) {
|
||||||
final document = RssFeed.parse(response.body);
|
throw Exception('No rss feed url for $name');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
final response = await http.get(Uri.parse(rssFeedUrl));
|
||||||
|
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;
|
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 db = DB.db;
|
||||||
final games = await db.rawQuery(
|
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 {
|
static Future<void> delete(Game game) async {
|
||||||
|
131
lib/main.dart
131
lib/main.dart
@@ -3,6 +3,7 @@ import 'package:gamer_updater/db.dart';
|
|||||||
import 'package:gamer_updater/game.dart';
|
import 'package:gamer_updater/game.dart';
|
||||||
import 'package:gamer_updater/widgets/new_game_card.dart';
|
import 'package:gamer_updater/widgets/new_game_card.dart';
|
||||||
import 'package:gamer_updater/widgets/game_card.dart';
|
import 'package:gamer_updater/widgets/game_card.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await DB.init();
|
await DB.init();
|
||||||
@@ -32,9 +33,9 @@ class MyApp extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
textTheme: const TextTheme(
|
textTheme: const TextTheme(
|
||||||
titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
titleLarge: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
|
||||||
bodyLarge: TextStyle(fontSize: 18),
|
bodyLarge: TextStyle(fontSize: 20),
|
||||||
bodyMedium: TextStyle(fontSize: 16),
|
bodyMedium: TextStyle(fontSize: 20),
|
||||||
bodySmall: TextStyle(fontSize: 14),
|
bodySmall: TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -53,7 +54,10 @@ class MyHomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
late List<Game> games = [];
|
late Map<String, Game> games = {};
|
||||||
|
String searchQuery = "";
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
Timer? _debounce;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -61,13 +65,46 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
_refreshGames();
|
_refreshGames();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
_debounce?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _refreshGames() async {
|
Future<void> _refreshGames() async {
|
||||||
final games = await GameRepository.getAll();
|
final games = await GameRepository.getAll();
|
||||||
|
games.forEach((key, game) {
|
||||||
|
game.updateActualVersion().then((_) {
|
||||||
|
GameRepository.upsert(game);
|
||||||
|
setState(() {
|
||||||
|
this.games[game.name] = game;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
setState(() {
|
setState(() {
|
||||||
this.games = games;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -78,46 +115,82 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
IconButton(icon: const Icon(Icons.refresh), onPressed: _refreshGames),
|
IconButton(icon: const Icon(Icons.refresh), onPressed: _refreshGames),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: Column(
|
||||||
onRefresh: _refreshGames,
|
children: [
|
||||||
child: GridView.builder(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8.0),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
child: TextField(
|
||||||
crossAxisCount: 3,
|
controller: _searchController,
|
||||||
childAspectRatio: 1.3,
|
decoration: InputDecoration(
|
||||||
crossAxisSpacing: 4,
|
hintText: 'Search games...',
|
||||||
mainAxisSpacing: 4,
|
prefixIcon: const Icon(Icons.search),
|
||||||
),
|
suffixIcon: searchQuery.isNotEmpty
|
||||||
itemCount: games.length + 1, // +1 for the new game card
|
? IconButton(
|
||||||
itemBuilder: (context, index) {
|
icon: const Icon(Icons.clear),
|
||||||
if (index == games.length) {
|
onPressed: () {
|
||||||
return NewGameCard(
|
_searchController.clear();
|
||||||
onGameCreated: (game) async {
|
|
||||||
game = await GameRepository.upsert(game);
|
|
||||||
setState(() {
|
setState(() {
|
||||||
games.add(game);
|
searchQuery = "";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
}
|
: null,
|
||||||
return GameCard(
|
border: OutlineInputBorder(
|
||||||
game: games[index],
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: _onSearchChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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 {
|
onGameUpdated: (game) async {
|
||||||
game = await GameRepository.upsert(game);
|
game = await GameRepository.upsert(game);
|
||||||
setState(() {
|
setState(() {
|
||||||
games[index] = game;
|
games[game.name] = game;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
await GameRepository.delete(games[index]);
|
await GameRepository.delete(game);
|
||||||
setState(() {
|
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;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,21 +27,8 @@ DateTime parseRfc822Date(String date) {
|
|||||||
final minute = int.parse(timeParts[1]);
|
final minute = int.parse(timeParts[1]);
|
||||||
final second = int.parse(timeParts[2]);
|
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
|
// Create the DateTime object
|
||||||
DateTime dateTime = DateTime(year, month, day, hour, minute, second);
|
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;
|
return dateTime;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gamer_updater/game.dart';
|
import 'package:gamer_updater/game.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
class GameCard extends StatefulWidget {
|
class GameCard extends StatefulWidget {
|
||||||
final Game game;
|
final Game game;
|
||||||
@@ -19,11 +20,19 @@ class GameCard extends StatefulWidget {
|
|||||||
State<GameCard> createState() => _GameCardState();
|
State<GameCard> createState() => _GameCardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin {
|
class _GameCardState extends State<GameCard>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late final AnimationController _controller;
|
late final AnimationController _controller;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
int _deleteClickCount = 0;
|
int _deleteClickCount = 0;
|
||||||
late TextEditingController _nameController;
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -33,12 +42,72 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
|
|||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
_nameController = TextEditingController(text: widget.game.name);
|
_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.trim(),
|
||||||
|
versionRegex: _versionRegexController.text,
|
||||||
|
lastPlayed: _lastPlayedController.text.trim(),
|
||||||
|
rssFeedUrl: _rssFeedUrlController.text.trim(),
|
||||||
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
|
_versionRegexController.dispose();
|
||||||
|
_rssFeedUrlController.dispose();
|
||||||
|
_lastPlayedController.dispose();
|
||||||
|
_nameFocus.dispose();
|
||||||
|
_versionRegexFocus.dispose();
|
||||||
|
_rssFeedUrlFocus.dispose();
|
||||||
|
_lastPlayedFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +116,15 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
|
|||||||
_controller.repeat();
|
_controller.repeat();
|
||||||
|
|
||||||
final updatedGame = widget.game;
|
final updatedGame = widget.game;
|
||||||
|
try {
|
||||||
await updatedGame.updateActualVersion();
|
await updatedGame.updateActualVersion();
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
widget.onGameUpdated(updatedGame);
|
widget.onGameUpdated(updatedGame);
|
||||||
|
|
||||||
_controller.stop();
|
_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.trim(),
|
||||||
|
versionRegex: _versionRegexController.text,
|
||||||
|
lastPlayed: _lastPlayedController.text.trim(),
|
||||||
|
rssFeedUrl: _rssFeedUrlController.text.trim(),
|
||||||
|
actualVersion: widget.game.actualVersion,
|
||||||
|
lastUpdated: widget.game.lastUpdated,
|
||||||
|
imageData: imageBytes,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
widget.onGameUpdated(updatedGame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isUpToDate = widget.game.actualVersion == widget.game.lastPlayed;
|
final isUpToDate = widget.game.actualVersion == widget.game.lastPlayed;
|
||||||
|
final hasImage = widget.game.imageData != null;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Stack(
|
||||||
padding: const EdgeInsets.all(16.0),
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _nameController,
|
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,
|
enabled: widget.isNameEditable,
|
||||||
decoration: const InputDecoration.collapsed(
|
decoration: const InputDecoration.collapsed(
|
||||||
hintText: 'New Game',
|
hintText: 'New Game',
|
||||||
),
|
),
|
||||||
onChanged: (value) => widget.onGameUpdated(Game(
|
onSubmitted: (_) => _nameFocus.unfocus(),
|
||||||
name: value,
|
|
||||||
versionRegex: widget.game.versionRegex,
|
|
||||||
lastPlayed: widget.game.lastPlayed,
|
|
||||||
rssFeedUrl: widget.game.rssFeedUrl,
|
|
||||||
actualVersion: widget.game.actualVersion,
|
|
||||||
lastUpdated: widget.game.lastUpdated,
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (!widget.isNameEditable)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.image),
|
||||||
|
color: hasImage ? Colors.white : null,
|
||||||
|
onPressed: _pickImage,
|
||||||
|
),
|
||||||
if (widget.onDelete != null)
|
if (widget.onDelete != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.delete,
|
Icons.delete,
|
||||||
color: _deleteClickCount > 0 ? Colors.red : null,
|
color:
|
||||||
|
_deleteClickCount > 0
|
||||||
|
? Colors.red
|
||||||
|
: (hasImage ? Colors.white : null),
|
||||||
),
|
),
|
||||||
onPressed: _handleDeleteClick,
|
onPressed: _handleDeleteClick,
|
||||||
),
|
),
|
||||||
|
if (!widget.isNameEditable)
|
||||||
RotationTransition(
|
RotationTransition(
|
||||||
turns: _controller,
|
turns: _controller,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
color: hasImage ? Colors.white : null,
|
||||||
|
),
|
||||||
onPressed: _isLoading ? null : _refreshVersion,
|
onPressed: _isLoading ? null : _refreshVersion,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -125,15 +262,23 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 200,
|
||||||
child: Text('Version:', style: Theme.of(context).textTheme.bodyLarge),
|
child: Text(
|
||||||
|
'Version:',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: hasImage ? Colors.white : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
widget.game.actualVersion.isEmpty ? 'Unknown' : widget.game.actualVersion,
|
widget.game.actualVersion.isEmpty
|
||||||
|
? 'Unknown'
|
||||||
|
: widget.game.actualVersion,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: isUpToDate ? Colors.green : Colors.red,
|
color: isUpToDate ? Colors.green : Colors.red,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -141,23 +286,39 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 200,
|
||||||
child: Text('Last Updated:', style: Theme.of(context).textTheme.bodyLarge),
|
child: Text(
|
||||||
|
'Last Updated:',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: hasImage ? Colors.white : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
widget.game.lastUpdated.isEmpty ? 'Never' : DateTime.parse(widget.game.lastUpdated).toString(),
|
widget.game.lastUpdated.isEmpty
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
? 'Never'
|
||||||
|
: DateTime.parse(widget.game.lastUpdated).toString(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: hasImage ? Colors.white : null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 200,
|
||||||
child: Text('Last Played:', style: Theme.of(context).textTheme.bodyLarge),
|
child: Text(
|
||||||
|
'Last Played:',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: hasImage ? Colors.white : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
widget.game.lastPlayed,
|
widget.game.lastPlayed,
|
||||||
@@ -168,46 +329,68 @@ class _GameCardState extends State<GameCard> with SingleTickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
controller: TextEditingController(text: widget.game.versionRegex),
|
controller: _versionRegexController,
|
||||||
decoration: const InputDecoration(labelText: 'Version Regex'),
|
focusNode: _versionRegexFocus,
|
||||||
onChanged: (value) => widget.onGameUpdated(Game(
|
style: TextStyle(color: hasImage ? Colors.white : null),
|
||||||
name: widget.isNameEditable ? _nameController.text : widget.game.name,
|
decoration: InputDecoration(
|
||||||
versionRegex: value,
|
labelText: 'Version Regex',
|
||||||
lastPlayed: widget.game.lastPlayed,
|
labelStyle: TextStyle(
|
||||||
rssFeedUrl: widget.game.rssFeedUrl,
|
color: hasImage ? Colors.white70 : null,
|
||||||
actualVersion: widget.game.actualVersion,
|
),
|
||||||
lastUpdated: widget.game.lastUpdated,
|
enabledBorder:
|
||||||
)),
|
hasImage
|
||||||
|
? const UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.white70),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => _versionRegexFocus.unfocus(),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: TextEditingController(text: widget.game.rssFeedUrl),
|
controller: _rssFeedUrlController,
|
||||||
decoration: const InputDecoration(labelText: 'RSS Feed URL'),
|
focusNode: _rssFeedUrlFocus,
|
||||||
onChanged: (value) => widget.onGameUpdated(Game(
|
style: TextStyle(color: hasImage ? Colors.white : null),
|
||||||
name: widget.isNameEditable ? _nameController.text : widget.game.name,
|
decoration: InputDecoration(
|
||||||
versionRegex: widget.game.versionRegex,
|
labelText: 'RSS Feed URL',
|
||||||
lastPlayed: widget.game.lastPlayed,
|
labelStyle: TextStyle(
|
||||||
rssFeedUrl: value,
|
color: hasImage ? Colors.white70 : null,
|
||||||
actualVersion: widget.game.actualVersion,
|
),
|
||||||
lastUpdated: widget.game.lastUpdated,
|
enabledBorder:
|
||||||
)),
|
hasImage
|
||||||
|
? const UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.white70),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => _rssFeedUrlFocus.unfocus(),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: TextEditingController(text: widget.game.lastPlayed),
|
controller: _lastPlayedController,
|
||||||
decoration: const InputDecoration(labelText: 'Last Played'),
|
focusNode: _lastPlayedFocus,
|
||||||
onChanged: (value) => widget.onGameUpdated(Game(
|
style: TextStyle(color: hasImage ? Colors.white : null),
|
||||||
name: widget.isNameEditable ? _nameController.text : widget.game.name,
|
decoration: InputDecoration(
|
||||||
versionRegex: widget.game.versionRegex,
|
labelText: 'Last Played',
|
||||||
lastPlayed: value,
|
labelStyle: TextStyle(
|
||||||
rssFeedUrl: widget.game.rssFeedUrl,
|
color: hasImage ? Colors.white70 : null,
|
||||||
actualVersion: widget.game.actualVersion,
|
),
|
||||||
lastUpdated: widget.game.lastUpdated,
|
enabledBorder:
|
||||||
)),
|
hasImage
|
||||||
|
? const UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Colors.white70),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => _lastPlayedFocus.unfocus(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,10 +5,7 @@ import 'package:gamer_updater/widgets/game_card.dart';
|
|||||||
class NewGameCard extends StatelessWidget {
|
class NewGameCard extends StatelessWidget {
|
||||||
final Function(Game) onGameCreated;
|
final Function(Game) onGameCreated;
|
||||||
|
|
||||||
const NewGameCard({
|
const NewGameCard({super.key, required this.onGameCreated});
|
||||||
super.key,
|
|
||||||
required this.onGameCreated,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
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);
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
@@ -5,6 +5,8 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import file_selector_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
}
|
}
|
||||||
|
153
pubspec.lock
153
pubspec.lock
@@ -41,6 +41,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
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:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -49,14 +57,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -73,6 +73,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -86,11 +118,24 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
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:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -107,14 +152,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
intl:
|
image_picker:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: image_picker
|
||||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -171,6 +272,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -179,14 +288,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
petitparser:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: plugin_platform_interface
|
||||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
version: "2.1.8"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -304,14 +413,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
xml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: xml
|
|
||||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.5.0"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.0 <4.0.0"
|
dart: ">=3.7.0 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.24.0"
|
||||||
|
@@ -36,7 +36,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
sqflite_common_ffi: ^2.3.5
|
sqflite_common_ffi: ^2.3.5
|
||||||
http: ^1.3.0
|
http: ^1.3.0
|
||||||
dart_rss: ^3.0.3
|
image_picker: ^1.0.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
62
release.sh
Normal file
62
release.sh
Normal 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"
|
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
Reference in New Issue
Block a user