From 5fa8c6496076422cd3f117978654e76edbda51dc Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 20 Mar 2026 16:59:45 +0100 Subject: Add path_provider package This is required to get the "Application Support" directory on Linux, Android, iOS, MacOS and Windows. This folder is used to store persistence of the Sia app. --- pubspec.lock | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- pubspec.yaml | 1 + 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 26b837f..cbe497a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -105,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -131,6 +147,22 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" image: dependency: transitive description: @@ -179,6 +211,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -211,6 +251,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.11.9" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" + url: "https://pub.dev" + source: hosted + version: "0.17.5" nested: dependency: transitive description: @@ -219,6 +267,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" path: dependency: transitive description: @@ -227,6 +283,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -235,6 +339,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" posix: dependency: transitive description: @@ -251,6 +371,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" sky_engine: dependency: transitive description: flutter @@ -336,6 +464,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -354,4 +490,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.10.4 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index ec03450..a02bef0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: flutter: sdk: flutter mqtt_client: ^10.11.3 + path_provider: ^2.1.5 provider: ^6.1.5 dev_dependencies: -- cgit v1.3 From 2aa1aa0d29fd551509d4a1ebb2a0a5470fc460d7 Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 20 Mar 2026 17:17:08 +0100 Subject: Add sqlite3 package Application data should be stored in a single sqlite3 database. This allows pretty mature and powerful data storage which works on Linux, Android, iOS, MacOS and Windows. --- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index cbe497a..a861593 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -392,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sqlite3: + dependency: "direct main" + description: + name: sqlite3 + sha256: caa693ad15a587a2b4fde093b728131a1827903872171089dedb16f7665d3a91 + url: "https://pub.dev" + source: hosted + version: "3.2.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a02bef0..a80996c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: mqtt_client: ^10.11.3 path_provider: ^2.1.5 provider: ^6.1.5 + sqlite3: ^3.2.0 dev_dependencies: flutter_test: -- cgit v1.3 From f021bffe2b37af563037195e0e93a74849cd860b Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 20 Mar 2026 19:58:57 +0100 Subject: Add package path This is added to join paths. --- pubspec.lock | 2 +- pubspec.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index a861593..30521d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -276,7 +276,7 @@ packages: source: hosted version: "9.3.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" diff --git a/pubspec.yaml b/pubspec.yaml index a80996c..605d9d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: flutter: sdk: flutter mqtt_client: ^10.11.3 + path: ^1.9.1 path_provider: ^2.1.5 provider: ^6.1.5 sqlite3: ^3.2.0 -- cgit v1.3 From 09330228991b73ede49ed186f74a2c4f306d2e20 Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 20 Mar 2026 17:02:39 +0100 Subject: Add reading server domain name from SQLite3 DB It is annoying to type the server domain name on every app start. This should be way more ergonomic. As a minimal first step reading from the database is implemented and tested with an externally injected database. --- lib/data.dart | 34 +++++++++++++++++++++++++++++++++- lib/ui.dart | 11 ++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/lib/data.dart b/lib/data.dart index 365f5bc..302908c 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -1,6 +1,11 @@ +import 'dart:io'; + import 'package:flutter/foundation.dart'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_server_client.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sqlite3/sqlite3.dart'; +import 'package:path/path.dart' as p; const int brokerPort = 1883; const String topicPrefix = 'sia'; @@ -46,9 +51,36 @@ class AppState with ChangeNotifier { Map contacts = {}; late MqttServerClient _client; + late Directory supportDir; String fqdn = ''; - AppState(); + AppState() { + loadPersistence(); + } + + void loadPersistence() async { + supportDir = await getApplicationSupportDirectory(); + String dbPath = p.join(supportDir.path, 'main.sqlite3'); + + final Database db = sqlite3.open(dbPath); + try { + final ResultSet result = db.select( + 'SELECT value FROM key_value WHERE key = \'server_fqdn\';' + ); + if (result.length == 1) { + fqdn = result[0]['value']; + notifyListeners(); + } + } catch (e) { + // loading persistence is optional + } finally { + db.close(); + } + } + + void setFqdn(String value) { + fqdn = value; + } void process(MachineEvent event) { MachineState lastState = state; diff --git a/lib/ui.dart b/lib/ui.dart index 9603562..3a98be9 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -41,8 +41,17 @@ class _ConnectionPageState extends State { final AppState provider = context.read(); controller = TextEditingController(text: provider.fqdn); + provider.addListener(() { + if (controller.text != provider.fqdn) { + controller.text = provider.fqdn; + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length), + ); + } + }); + controller.addListener(() { - provider.fqdn = controller.text; + provider.setFqdn(controller.text); }); } -- cgit v1.3 From 09c34e0f373d48f5993598c76fa176c8ed2fd4cd Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 20 Mar 2026 21:10:12 +0100 Subject: Add DB schema version check This version is only compatible with the schema version 0. No migrations are implemented. Thus the DB is only accessed if the schema version saved as user_version is as expected. --- lib/data.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/data.dart b/lib/data.dart index 302908c..c9c8b4d 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -64,7 +64,13 @@ class AppState with ChangeNotifier { final Database db = sqlite3.open(dbPath); try { - final ResultSet result = db.select( + ResultSet result = db.select('PRAGMA user_version;'); + final int version = result.first.values.first as int; + if (version != 0) { + return; // DB schema version 0 required, no migrations implemented + } + + result = db.select( 'SELECT value FROM key_value WHERE key = \'server_fqdn\';' ); if (result.length == 1) { @@ -72,7 +78,7 @@ class AppState with ChangeNotifier { notifyListeners(); } } catch (e) { - // loading persistence is optional + return; } finally { db.close(); } -- cgit v1.3 From a406df320080bf31e32cb181136928793cb5d37b Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 3 Apr 2026 19:49:29 +0200 Subject: Add *_futures to analysis_options.yaml These options should avoid making typical mistakes in asynchronous code. --- analysis_options.yaml | 2 ++ lib/data.dart | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index c8ed5db..c7d3757 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,3 +8,5 @@ linter: prefer_final_fields: true avoid_dynamic_calls: true unnecessary_null_checks: true + unawaited_futures: true + discarded_futures: true diff --git a/lib/data.dart b/lib/data.dart index c9c8b4d..7d5f938 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:mqtt_client/mqtt_client.dart'; @@ -141,7 +142,7 @@ class AppState with ChangeNotifier { _client.onAutoReconnected = _onAutoReconnected; try { - _client.connect(); + unawaited(_client.connect()); } catch (e) { _client.disconnect(); return; -- cgit v1.3 From 2dc31225e08260a4a8f84fd61623e28f1c8c001e Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 3 Apr 2026 21:29:39 +0200 Subject: Introduce class DB This centralises database related code in one class. --- lib/data.dart | 39 +++++++++++---------------------------- lib/db.dart | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 28 deletions(-) create mode 100644 lib/db.dart diff --git a/lib/data.dart b/lib/data.dart index 7d5f938..e4d7524 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -4,9 +4,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_server_client.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:sqlite3/sqlite3.dart'; -import 'package:path/path.dart' as p; + +import 'db.dart'; const int brokerPort = 1883; const String topicPrefix = 'sia'; @@ -28,6 +27,8 @@ enum MachineEvent { } class AppState with ChangeNotifier { + final DB db = DB(); + static const Map> machine = > { MachineState.init: { MachineEvent.connect: MachineState.disconnected, @@ -56,33 +57,15 @@ class AppState with ChangeNotifier { String fqdn = ''; AppState() { - loadPersistence(); + unawaited(loadPersistence()); } - void loadPersistence() async { - supportDir = await getApplicationSupportDirectory(); - String dbPath = p.join(supportDir.path, 'main.sqlite3'); - - final Database db = sqlite3.open(dbPath); - try { - ResultSet result = db.select('PRAGMA user_version;'); - final int version = result.first.values.first as int; - if (version != 0) { - return; // DB schema version 0 required, no migrations implemented - } - - result = db.select( - 'SELECT value FROM key_value WHERE key = \'server_fqdn\';' - ); - if (result.length == 1) { - fqdn = result[0]['value']; - notifyListeners(); - } - } catch (e) { - return; - } finally { - db.close(); - } + Future loadPersistence() async { + await db.connect(); + String? dbFqdn = await db.getServerFqdn(); + if (dbFqdn == null) return; + fqdn = dbFqdn; + notifyListeners(); } void setFqdn(String value) { diff --git a/lib/db.dart b/lib/db.dart new file mode 100644 index 0000000..8afd3eb --- /dev/null +++ b/lib/db.dart @@ -0,0 +1,55 @@ +import 'dart:io'; +import 'dart:async'; + +import 'package:path_provider/path_provider.dart'; +import 'package:sqlite3/sqlite3.dart'; +import 'package:path/path.dart' as p; + +class DB { + final Completer _dbCompleter = Completer(); + + Future connect() async { + String path = await _getDbPath(); + Database candidate = sqlite3.open(path); + + int? userVersion = _getUserVersion(candidate); + if (userVersion == null || userVersion != 0) { + _dbCompleter.complete(null); + return; + } + + _dbCompleter.complete(candidate); + } + + void dispose() async { + if (_dbCompleter.isCompleted == false) { + return; + } + Database? db = await _dbCompleter.future; + if (db == null) return; + db.close(); + } + + static Future _getDbPath() async { + Directory supportDir = await getApplicationSupportDirectory(); + return p.join(supportDir.path, 'main.sqlite3'); + } + + static int? _getUserVersion(Database db) { + ResultSet result = db.select('PRAGMA user_version;'); + if (result.length != 1) return null; + return result.first.values.first as int; + } + + Future getServerFqdn() async { + Database? db = await _dbCompleter.future; + if (db == null) return null; + + ResultSet result = db.select( + 'SELECT value FROM key_value WHERE key = \'server_fqdn\';' + ); + if (result.length != 1) return null; + + return result[0]['value']; + } +} -- cgit v1.3 From 34d52bf961071348d8262e6d08d1703530ff8556 Mon Sep 17 00:00:00 2001 From: xengineering Date: Mon, 6 Apr 2026 11:04:53 +0200 Subject: Implement SQL migrations This allows to update the SQL schema incrementally. --- lib/db.dart | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/db.dart b/lib/db.dart index 8afd3eb..a39db1d 100644 --- a/lib/db.dart +++ b/lib/db.dart @@ -7,16 +7,22 @@ import 'package:path/path.dart' as p; class DB { final Completer _dbCompleter = Completer(); + static const List migrations = [ + ''' +PRAGMA user_version = 1; +CREATE TABLE "key_value" ( + "key" TEXT NOT NULL UNIQUE, + "value" TEXT, + PRIMARY KEY("key") +); + ''', + ]; Future connect() async { String path = await _getDbPath(); Database candidate = sqlite3.open(path); - int? userVersion = _getUserVersion(candidate); - if (userVersion == null || userVersion != 0) { - _dbCompleter.complete(null); - return; - } + migrate(candidate); _dbCompleter.complete(candidate); } @@ -35,6 +41,17 @@ class DB { return p.join(supportDir.path, 'main.sqlite3'); } + static void migrate(Database db) { + for (int i=0; i Date: Mon, 6 Apr 2026 16:57:58 +0200 Subject: Implement setting server_fqdn This persists the used server_fqdn on connection. This makes sure the user does not need to specify the FQDN every time. --- lib/data.dart | 1 + lib/db.dart | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/data.dart b/lib/data.dart index e4d7524..ca350e5 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -70,6 +70,7 @@ class AppState with ChangeNotifier { void setFqdn(String value) { fqdn = value; + unawaited(db.setServerFqdn(value)); } void process(MachineEvent event) { diff --git a/lib/db.dart b/lib/db.dart index a39db1d..0b2cf7a 100644 --- a/lib/db.dart +++ b/lib/db.dart @@ -69,4 +69,22 @@ CREATE TABLE "key_value" ( return result[0]['value']; } + + Future setServerFqdn(String value) async { + Database? db = await _dbCompleter.future; + if (db == null) return; + + String? current = await getServerFqdn(); + if (current == null) { + db.execute( + 'INSERT INTO key_value (key, value) VALUES (\'server_fqdn\', ?);', + [value], + ); + } else { + db.execute( + 'UPDATE key_value SET value = ? WHERE key = \'server_fqdn\';', + [value], + ); + } + } } -- cgit v1.3