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)
|
||||
- [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:
|
||||

|
||||
|
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,
|
||||
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);
|
||||
|
@@ -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 {
|
||||
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 (rssFeedUrl.isEmpty) {
|
||||
throw Exception('No rss feed url for $name');
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
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/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,46 +115,82 @@ 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,
|
||||
),
|
||||
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);
|
||||
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(() {
|
||||
games.add(game);
|
||||
searchQuery = "";
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
return GameCard(
|
||||
game: games[index],
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
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 {
|
||||
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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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.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
|
||||
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.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
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
|
||||
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"
|
||||
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"
|
||||
|
@@ -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
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 <file_selector_windows/file_selector_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
Reference in New Issue
Block a user