Compare commits
	
		
			21 Commits
		
	
	
		
			1ec8fa1f0d
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ad04a6317b | |||
| 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,130 @@ 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(
 | 
			
		||||
                        child:
 | 
			
		||||
                            widget.isNameEditable
 | 
			
		||||
                                ? TextField(
 | 
			
		||||
                                  controller: _nameController,
 | 
			
		||||
                    style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
                    enabled: widget.isNameEditable,
 | 
			
		||||
                                  focusNode: _nameFocus,
 | 
			
		||||
                                  style: Theme.of(
 | 
			
		||||
                                    context,
 | 
			
		||||
                                  ).textTheme.titleLarge?.copyWith(
 | 
			
		||||
                                    color: hasImage ? Colors.white : null,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  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(),
 | 
			
		||||
                                )
 | 
			
		||||
                                : SelectableText(
 | 
			
		||||
                                  widget.game.name,
 | 
			
		||||
                                  style: Theme.of(
 | 
			
		||||
                                    context,
 | 
			
		||||
                                  ).textTheme.titleLarge?.copyWith(
 | 
			
		||||
                                    color: hasImage ? Colors.white : null,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                      ),
 | 
			
		||||
                      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 +280,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 +304,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 +347,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