diff --git a/lib/db.dart b/lib/db.dart new file mode 100644 index 0000000..5922c89 --- /dev/null +++ b/lib/db.dart @@ -0,0 +1,79 @@ +import 'dart:io' show Platform, Directory; +import 'package:path/path.dart' as path; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +const settingsDir = '.gamer-updater'; +const dbFileName = 'data.db'; + +class DB { + static late Database db; + + // SQL schema definition + static const String _schema = ''' +CREATE TABLE IF NOT EXISTS games ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + actual_version TEXT NOT NULL, + last_played TEXT NOT NULL, + rss_feed_url TEXT NOT NULL, + version_regex TEXT NOT NULL, + last_updated TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_games_name ON games (name); +CREATE UNIQUE INDEX IF NOT EXISTS idx_games_name_unique ON games (name); +'''; + + static Future _getDatabasePath() async { + print('Attempting to get database path...'); + if (Platform.isWindows || Platform.isLinux) { + // Get user's home directory + final home = + Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + if (home == null) { + throw Exception('Could not find home directory'); + } + print('Home directory found: home'); + + final dbDir = Directory(path.join(home, settingsDir)); + if (!await dbDir.exists()) { + await dbDir.create(recursive: true); + print('$settingsDir directory created'); + } else { + print('$settingsDir directory already exists'); + } + + return path.join(dbDir.path, dbFileName); + } else { + // Default path for other platforms + final databasesPath = await databaseFactoryFfi.getDatabasesPath(); + print('Using default databases path: databasesPath'); + return path.join(databasesPath, dbFileName); + } + } + + static Future init() async { + print('Starting database initialization...'); + sqfliteFfiInit(); + + final dbPath = await _getDatabasePath(); + print('Database path: dbPath'); + + try { + db = await databaseFactoryFfi.openDatabase( + dbPath, + options: OpenDatabaseOptions( + version: 1, + onCreate: (db, version) async { + print('Creating database schema...'); + await db.execute(_schema); + print('Database schema created successfully'); + }, + ), + ); + print('Database opened and initialized'); + } catch (e) { + print('Failed to initialize database: e'); + rethrow; + } + } +} diff --git a/lib/game.dart b/lib/game.dart new file mode 100644 index 0000000..583e2c9 --- /dev/null +++ b/lib/game.dart @@ -0,0 +1,88 @@ +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'; + +class Game { + final String name; + final String versionRegex; + final RegExp _internalVersionRegex; + final String lastPlayed; + String actualVersion; + String lastUpdated; + final String rssFeedUrl; + + Game({ + required this.name, + required this.versionRegex, + required this.lastPlayed, + this.actualVersion = '', + required this.rssFeedUrl, + this.lastUpdated = '', + }) : _internalVersionRegex = RegExp(versionRegex); + + Future 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; + } + } + } +} + +class GameRepository { + static Future upsert(Game game) async { + final db = await DB.db; + await db.rawInsert( + ''' +INSERT INTO games +(name, actual_version, last_played, rss_feed_url, version_regex, last_updated) +VALUES +(?, ?, ?, ?, ?, ?) +ON CONFLICT(name) DO UPDATE SET +actual_version = excluded.actual_version, +last_played = excluded.last_played, +rss_feed_url = excluded.rss_feed_url, +version_regex = excluded.version_regex, +last_updated = excluded.last_updated +''', + [ + game.name, + game.actualVersion, + game.lastPlayed, + game.rssFeedUrl, + game.versionRegex, + game.lastUpdated, + ], + ); + return game; + } +} + +//CREATE TABLE IF NOT EXISTS games ( +// id INTEGER PRIMARY KEY AUTOINCREMENT, +// name TEXT NOT NULL, +// actual_version TEXT NOT NULL, +// last_played TEXT NOT NULL, +// rss_feed_url TEXT NOT NULL, +// version_regex TEXT NOT NULL, +// last_updated TEXT NOT NULL +//); +//CREATE INDEX IF NOT EXISTS idx_games_name ON games (name); diff --git a/lib/main.dart b/lib/main.dart index a964b0b..cc9fbc7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:gamer_updater/db.dart'; +import 'package:gamer_updater/game.dart'; + +void main() async { + await DB.init(); + + var game = Game( + name: 'Rimworld', + versionRegex: r'(\d+\.\d+\.\d+)', + lastPlayed: '1.4.3704', + rssFeedUrl: + 'https://store.steampowered.com/feeds/news/app/294100/?cc=HR&l=english', + ); + await game.updateActualVersion(); + await GameRepository.upsert(game); -void main() { runApp(const MyApp()); } @@ -14,6 +28,19 @@ class MyApp extends StatelessWidget { theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), ), + darkTheme: ThemeData( + brightness: Brightness.dark, + useMaterial3: true, + scaffoldBackgroundColor: Colors.black, + colorSchemeSeed: Colors.black, + highlightColor: Colors.deepPurple, + textTheme: TextTheme( + bodyLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + bodyMedium: TextStyle(fontSize: 20), + bodySmall: TextStyle(fontSize: 16), + ), + ), + themeMode: ThemeMode.system, home: const MyHomePage(title: 'Gamer Updater'), ); } @@ -28,14 +55,6 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - _counter++; - }); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -46,20 +65,9 @@ class _MyHomePageState extends State { body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], + children: [const Text('Hello cyka')], ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), ); } } diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..7d36e4a --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,47 @@ +DateTime parseRfc822Date(String date) { + // Split the date string into components + final parts = date.split(' '); + + // Map the month names to their corresponding numbers + const monthMap = { + 'Jan': 1, + 'Feb': 2, + 'Mar': 3, + 'Apr': 4, + 'May': 5, + 'Jun': 6, + 'Jul': 7, + 'Aug': 8, + 'Sep': 9, + 'Oct': 10, + 'Nov': 11, + 'Dec': 12, + }; + + // Extract the components + final day = int.parse(parts[1]); + final month = monthMap[parts[2]]!; + final year = int.parse(parts[3]); + final timeParts = parts[4].split(':'); + final hour = int.parse(timeParts[0]); + 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; +} diff --git a/pubspec.lock b/pubspec.lock index c2c57f7..9e272de 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ 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: @@ -57,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" flutter: dependency: "direct main" description: flutter @@ -75,6 +91,30 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: "direct main" + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" leak_tracker: dependency: transitive description: @@ -139,6 +179,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" sky_engine: dependency: transitive description: flutter @@ -152,6 +200,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" + url: "https://pub.dev" + source: hosted + version: "2.5.5" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d" + url: "https://pub.dev" + source: hosted + version: "2.7.4" stack_trace: dependency: transitive description: @@ -176,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://pub.dev" + source: hosted + version: "3.3.1" term_glyph: dependency: transitive description: @@ -192,6 +272,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -208,6 +296,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.1" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + 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" diff --git a/pubspec.yaml b/pubspec.yaml index 0b12c03..dd99d6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,9 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + sqflite_common_ffi: ^2.3.5 + http: ^1.3.0 + dart_rss: ^3.0.3 dev_dependencies: flutter_test: