diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e2a08f6f..39f4f3a1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -17,6 +17,11 @@ PODS: - FlutterMacOS - pointer_interceptor_ios (0.0.1): - Flutter + - powersync-sqlite-core (0.4.7) + - powersync_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - powersync-sqlite-core (~> 0.4.6) - rive_common (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -63,6 +68,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) + - powersync_flutter_libs (from `.symlinks/plugins/powersync_flutter_libs/darwin`) - rive_common (from `.symlinks/plugins/rive_common/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) @@ -71,6 +77,7 @@ DEPENDENCIES: SPEC REPOS: trunk: + - powersync-sqlite-core - sqlite3 EXTERNAL SOURCES: @@ -92,6 +99,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" pointer_interceptor_ios: :path: ".symlinks/plugins/pointer_interceptor_ios/ios" + powersync_flutter_libs: + :path: ".symlinks/plugins/powersync_flutter_libs/darwin" rive_common: :path: ".symlinks/plugins/rive_common/ios" shared_preferences_foundation: @@ -113,6 +122,8 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed + powersync-sqlite-core: 7d97e321b00adbbc4d87cc8bab3a8895c855327c + powersync_flutter_libs: 19fc6b96ff8155ffea72a08990f6c9f2e712b8a6 rive_common: dd421daaf9ae69f0125aa761dd96abd278399952 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b diff --git a/lib/api_client.dart b/lib/api_client.dart new file mode 100644 index 00000000..3f2371e5 --- /dev/null +++ b/lib/api_client.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:wger/helpers/shared_preferences.dart'; + +import 'helpers/consts.dart'; + +class ApiClient { + final _logger = Logger('powersync-ApiClient'); + + final String baseUrl; + + ApiClient(this.baseUrl); + + /// Returns a powersync JWT token token + /// + /// Note that at the moment we use the permanent API token for authentication + /// but this should be probably changed to the wger API JWT tokens in the + /// future since they are not permanent and could be easily revoked. + Future> getPowersyncToken() async { + final prefs = PreferenceHelper.asyncPref; + + final apiData = json.decode((await prefs.getString(PREFS_USER))!); + _logger.info('posting our token "${apiData["token"]}" to $baseUrl/api/v2/powersync-token'); + final response = await http.get( + Uri.parse('$baseUrl/api/v2/powersync-token'), + headers: { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: 'Token ${apiData["token"]}', + }, + ); + _logger.info('response: status ${response.statusCode}, body ${response.body}'); + if (response.statusCode == 200) { + _logger.log(Level.ALL, response.body); + return json.decode(response.body); + } + throw Exception('Failed to fetch token'); + } + + Future upsert(Map record) async { + await http.put( + Uri.parse('$baseUrl/api/upload-powersync-data'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } + + Future update(Map record) async { + await http.patch( + Uri.parse('$baseUrl/api/upload-powersync-data'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } + + Future delete(Map record) async { + await http.delete( + Uri.parse('$baseUrl/api/v2/upload-powersync-data'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } +} diff --git a/lib/helpers/measurements.dart b/lib/helpers/measurements.dart index bcde38fb..9f318f0e 100644 --- a/lib/helpers/measurements.dart +++ b/lib/helpers/measurements.dart @@ -32,7 +32,9 @@ extension MeasurementChartEntryListExtensions on List { // this also helps with computing delta's across the entire window List whereDateWithInterpolation(DateTime start, DateTime? end) { // Make sure our list is sorted by date - sort((a, b) => a.date.compareTo(b.date)); + // Avoid mutating the original list (it may be unmodifiable/const). Work on a copy. + final List sorted = [...this]; + sorted.sort((a, b) => a.date.compareTo(b.date)); // Initialize result list final List result = []; @@ -46,7 +48,7 @@ extension MeasurementChartEntryListExtensions on List { MeasurementChartEntry? lastBeforeEnd; // Single pass through the data - for (final entry in this) { + for (final entry in sorted) { if (entry.date.isSameDayAs(start)) { hasEntryOnStartDay = true; } diff --git a/lib/models/body_weight/weight_entry.dart b/lib/models/body_weight/weight_entry.dart index d5343cd6..4e691da7 100644 --- a/lib/models/body_weight/weight_entry.dart +++ b/lib/models/body_weight/weight_entry.dart @@ -17,6 +17,7 @@ */ import 'package:json_annotation/json_annotation.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; import 'package:wger/helpers/json.dart'; part 'weight_entry.g.dart'; @@ -24,7 +25,7 @@ part 'weight_entry.g.dart'; @JsonSerializable() class WeightEntry { @JsonKey(required: true) - int? id; + String? id; @JsonKey(required: true, fromJson: stringToNum, toJson: numToString) late num weight = 0; @@ -32,7 +33,7 @@ class WeightEntry { @JsonKey(required: true) late DateTime date; - WeightEntry({this.id, weight, DateTime? date}) { + WeightEntry({this.id, num? weight, DateTime? date}) { this.date = date ?? DateTime.now(); if (weight != null) { @@ -40,7 +41,15 @@ class WeightEntry { } } - WeightEntry copyWith({int? id, int? weight, DateTime? date}) => WeightEntry( + factory WeightEntry.fromRow(sqlite.Row row) { + return WeightEntry( + id: row['uuid'], + date: DateTime.parse(row['date']), + weight: row['weight'], + ); + } + + WeightEntry copyWith({String? id, int? weight, DateTime? date}) => WeightEntry( id: id, weight: weight ?? this.weight, date: date ?? this.date, diff --git a/lib/models/body_weight/weight_entry.g.dart b/lib/models/body_weight/weight_entry.g.dart index 42281afa..e783b107 100644 --- a/lib/models/body_weight/weight_entry.g.dart +++ b/lib/models/body_weight/weight_entry.g.dart @@ -9,7 +9,7 @@ part of 'weight_entry.dart'; WeightEntry _$WeightEntryFromJson(Map json) { $checkKeys(json, requiredKeys: const ['id', 'weight', 'date']); return WeightEntry( - id: (json['id'] as num?)?.toInt(), + id: json['id'] as String?, weight: stringToNum(json['weight'] as String?), date: json['date'] == null ? null : DateTime.parse(json['date'] as String), ); diff --git a/lib/models/powersync/schema.dart b/lib/models/powersync/schema.dart new file mode 100644 index 00000000..9a71876e --- /dev/null +++ b/lib/models/powersync/schema.dart @@ -0,0 +1,22 @@ +import 'package:powersync/powersync.dart'; + +const tableMuscles = 'exercises_muscle'; +const tableBodyWeights = 'weight_weightentry'; + +Schema schema = const Schema([ + Table( + tableMuscles, + [ + Column.text('name'), + Column.text('name_en'), + Column.text('is_front'), + ], + ), + Table( + tableBodyWeights, + [ + Column.real('weight'), + Column.text('date'), + ], + ), +]); diff --git a/lib/powersync.dart b/lib/powersync.dart new file mode 100644 index 00000000..6bab507f --- /dev/null +++ b/lib/powersync.dart @@ -0,0 +1,122 @@ +// This file performs setup of the PowerSync database +import 'package:logging/logging.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:powersync/powersync.dart'; +import 'package:wger/api_client.dart'; +import 'package:wger/models/powersync/schema.dart'; + +final logger = Logger('powersync-django'); + +/// Postgres Response codes that we cannot recover from by retrying. +final List fatalResponseCodes = [ + // Class 22 — Data Exception + // Examples include data type mismatch. + RegExp(r'^22...$'), + // Class 23 — Integrity Constraint Violation. + // Examples include NOT NULL, FOREIGN KEY and UNIQUE violations. + RegExp(r'^23...$'), + // INSUFFICIENT PRIVILEGE - typically a row-level security violation + RegExp(r'^42501$'), +]; + +class DjangoConnector extends PowerSyncBackendConnector { + PowerSyncDatabase db; + String baseUrl; + String powersyncUrl; + late ApiClient apiClient; + + DjangoConnector(this.db, this.baseUrl, this.powersyncUrl) { + apiClient = ApiClient(baseUrl); + } + + /// Get a token to authenticate against the PowerSync instance. + @override + Future fetchCredentials() async { + // Somewhat contrived to illustrate usage, see auth docs here: + // https://docs.powersync.com/usage/installation/authentication-setup/custom + // final wgerSession = await apiClient.getWgerJWTToken(); + final session = await apiClient.getPowersyncToken(); + // note: we don't set userId and expires property here. not sure if needed + logger.info('Powersync Url: $powersyncUrl'); + return PowerSyncCredentials(endpoint: powersyncUrl, token: session['token']); + } + + // Upload pending changes to Postgres via Django backend + // this is generic. on the django side we inspect the request and do model-specific operations + // would it make sense to do api calls here specific to the relevant model? (e.g. put to a todo-specific endpoint) + @override + Future uploadData(PowerSyncDatabase database) async { + final transaction = await database.getNextCrudTransaction(); + + if (transaction == null) { + return; + } + + try { + for (final op in transaction.crud) { + final record = { + 'table': op.table, + 'data': {'id': op.id, ...?op.opData}, + }; + + logger.fine('DIETER Uploading record', record); + + switch (op.op) { + case UpdateType.put: + await apiClient.upsert(record); + break; + case UpdateType.patch: + await apiClient.update(record); + break; + case UpdateType.delete: + await apiClient.delete(record); + break; + } + } + await transaction.complete(); + } on Exception catch (e) { + logger.severe('Error uploading data', e); + // Error may be retryable - e.g. network error or temporary server error. + // Throwing an error here causes this call to be retried after a delay. + rethrow; + } + } +} + +/// Global reference to the database +late final PowerSyncDatabase db; + +// Hacky flag to ensure the database is only initialized once, better to do this with listeners +bool _dbInitialized = false; + +Future getDatabasePath() async { + final dir = await getApplicationSupportDirectory(); + return join(dir.path, 'powersync-demo.db'); +} + +// opens the database and connects if logged in +Future openDatabase(bool connect, String baseUrl, String powersyncUrl) async { + // Open the local database + if (!_dbInitialized) { + db = PowerSyncDatabase(schema: schema, path: await getDatabasePath(), logger: attachedLogger); + await db.initialize(); + _dbInitialized = true; + } + + if (connect) { + // If the user is already logged in, connect immediately. + // Otherwise, connect once logged in. + + final currentConnector = DjangoConnector(db, baseUrl, powersyncUrl); + db.connect(connector: currentConnector); + + // TODO: should we respond to login state changing? like here: + // https://www.powersync.com/blog/flutter-tutorial-building-an-offline-first-chat-app-with-supabase-and-powersync#implement-auth-methods + } +} + +/// Explicit sign out - clear database and log out. +Future logout() async { + await db.disconnectAndClear(); +} diff --git a/lib/providers/body_weight_powersync.dart b/lib/providers/body_weight_powersync.dart new file mode 100644 index 00000000..57e50345 --- /dev/null +++ b/lib/providers/body_weight_powersync.dart @@ -0,0 +1,17 @@ +// Helper to read body weight entries from PowerSync local database and convert to WeightEntry + +import 'package:logging/logging.dart'; +import 'package:wger/models/body_weight/weight_entry.dart'; +import 'package:wger/models/powersync/schema.dart'; +import 'package:wger/powersync.dart' as ps; + +final _log = Logger('body_weight_powersync'); + +/// Watch the body weight entries as a stream. +Stream> watchBodyWeightEntries({ + Duration pollInterval = const Duration(seconds: 2), +}) { + return ps.db.watch('SELECT * FROM $tableBodyWeights ORDER BY id').map((results) { + return results.map(WeightEntry.fromRow).toList(growable: false); + }); +} diff --git a/lib/providers/body_weight_repository.dart b/lib/providers/body_weight_repository.dart index a770678c..33c68a06 100644 --- a/lib/providers/body_weight_repository.dart +++ b/lib/providers/body_weight_repository.dart @@ -47,13 +47,13 @@ class BodyWeightRepository { _logger.info('Repository: updating weight entry ${entry.id}'); await baseProvider.patch( entry.toJson(), - baseProvider.makeUrl(BODY_WEIGHT_URL, id: entry.id), + baseProvider.makeUrl(BODY_WEIGHT_URL, id: int.parse(entry.id!)), ); } /// Deletes a weight entry on the server - Future delete(int id) async { + Future delete(String id) async { _logger.info('Repository: deleting weight entry $id'); - return await baseProvider.deleteRequest(BODY_WEIGHT_URL, id); + return await baseProvider.deleteRequest(BODY_WEIGHT_URL, int.parse(id)); } } diff --git a/lib/providers/body_weight_riverpod.dart b/lib/providers/body_weight_riverpod.dart index b99a25a1..8b5e80a7 100644 --- a/lib/providers/body_weight_riverpod.dart +++ b/lib/providers/body_weight_riverpod.dart @@ -1,6 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:wger/models/body_weight/weight_entry.dart'; import 'package:wger/providers/auth.dart'; +import 'package:wger/providers/body_weight_powersync.dart'; import 'package:wger/providers/body_weight_repository.dart'; import 'package:wger/providers/body_weight_state.dart'; import 'package:wger/providers/wger_base_riverpod.dart'; @@ -23,3 +24,16 @@ final bodyWeightStateProvider = return BodyWeightState(repo); }, ); + +/// StreamProvider that exposes live updates from the local PowerSync DB for +/// body weight entries. Widgets can `ref.watch(bodyWeightStreamProvider(auth))` +/// to get an `AsyncValue>` that updates whenever PowerSync +/// reports changes. +final bodyWeightStreamProvider = StreamProvider.family, AuthProvider>( + (ref, auth) { + // The stream itself is independent of Auth for now; auth is kept as a + // family parameter for symmetry with other providers and potential + // future auth-dependent logic (e.g. different DB instances per user). + return watchBodyWeightEntries(); + }, +); diff --git a/lib/providers/body_weight_state.dart b/lib/providers/body_weight_state.dart index b63012d5..9dd77433 100644 --- a/lib/providers/body_weight_state.dart +++ b/lib/providers/body_weight_state.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:wger/models/body_weight/weight_entry.dart'; @@ -59,7 +61,7 @@ class BodyWeightState extends StateNotifier> { } } - Future deleteEntry(int id) async { + Future deleteEntry(String id) async { final idx = state.indexWhere((e) => e.id == id); if (idx < 0) { return; diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index 27affcdd..824b4444 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -23,8 +23,8 @@ import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:rive/rive.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/powersync.dart'; import 'package:wger/providers/auth.dart'; -import 'package:wger/providers/body_weight_riverpod.dart'; import 'package:wger/providers/exercises.dart'; import 'package:wger/providers/gallery.dart'; import 'package:wger/providers/measurement.dart'; @@ -57,6 +57,10 @@ class _HomeTabsScreenState extends riverpod.ConsumerState @override void initState() { super.initState(); + + // do we need to await this? or if it's async, how do we handle failures? + _setupPowersync(); + // Loading data here, since the build method can be called more than once _initialData = _loadEntries(); } @@ -67,6 +71,25 @@ class _HomeTabsScreenState extends riverpod.ConsumerState }); } + Future _setupPowersync() async { + final authProvider = context.read(); + final baseUrl = authProvider.serverUrl!; + final powerSyncUrl = baseUrl.replaceAll(':8000', ':8080'); + + await openDatabase(false, baseUrl, powerSyncUrl); + + final connector = DjangoConnector(db, baseUrl, powerSyncUrl); + // try { + // TODO: should we cache these credentials? that's what their demo does? + // we could maybe get the initial token from the /api/v2/login call + final credentials = await connector.fetchCredentials(); + widget._logger.fine('fetched credentials: $credentials'); + await openDatabase(true, baseUrl, powerSyncUrl); + // } catch (e) { + // widget._logger.warning('failed to fetchCredentials: $e'); + // }/ + } + final _screenList = [ const DashboardScreen(), const RoutineListScreen(), @@ -119,7 +142,7 @@ class _HomeTabsScreenState extends riverpod.ConsumerState measurementProvider.fetchAndSetAllCategoriesAndEntries(), ]); // fetch weights separately using Riverpod notifier - await ref.read(bodyWeightStateProvider(authProvider).notifier).fetchAndSetEntries(); + // await ref.read(bodyWeightStateProvider(authProvider).notifier).fetchAndSetEntries(); // // Current nutritional plan diff --git a/lib/widgets/weight/weight_overview.dart b/lib/widgets/weight/weight_overview.dart index dccac6ec..8106e426 100644 --- a/lib/widgets/weight/weight_overview.dart +++ b/lib/widgets/weight/weight_overview.dart @@ -40,99 +40,124 @@ class WeightOverview extends riverpod.ConsumerWidget { final plans = context.read().items; final auth = context.read(); - final entriesList = ref.watch(bodyWeightStateProvider(auth)); - final entriesAll = entriesList.map((e) => MeasurementChartEntry(e.weight, e.date)).toList(); - final entries7dAvg = moving7dAverage(entriesAll); + final entriesAsync = ref.watch(bodyWeightStreamProvider(auth)); - final unit = weightUnit(profile!.isMetric, context); + // Handle stream states (loading/error/data) and reuse the existing UI + return entriesAsync.when( + data: (entriesList) { + final entriesAll = entriesList.map((e) => MeasurementChartEntry(e.weight, e.date)).toList(); + final entries7dAvg = moving7dAverage(entriesAll); - return Column( - children: [ - ...getOverviewWidgetsSeries( - AppLocalizations.of(context).weight, - entriesAll, - entries7dAvg, - plans, - unit, - context, - ), - TextButton( - onPressed: () => Navigator.pushNamed( - context, - '/measurement-categories', - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text(AppLocalizations.of(context).measurements), - const Icon(Icons.chevron_right), - ], - ), - ), - SizedBox( - height: 300, - child: RefreshIndicator( - onRefresh: () => ref.read(bodyWeightStateProvider(auth).notifier).fetchAndSetEntries(), - child: ListView.builder( - padding: const EdgeInsets.all(10.0), - itemCount: entriesList.length, - itemBuilder: (context, index) { - final currentEntry = entriesList[index]; - return Card( - child: ListTile( - title: Text( - '${numberFormat.format(currentEntry.weight)} ${weightUnit(profile.isMetric, context)}', - ), - subtitle: Text( - DateFormat.yMd( - Localizations.localeOf(context).languageCode, - ).add_Hm().format(currentEntry.date), - ), - trailing: PopupMenuButton( - itemBuilder: (BuildContext context) { - return [ - PopupMenuItem( - child: Text(AppLocalizations.of(context).edit), - onTap: () => Navigator.pushNamed( - context, - FormScreen.routeName, - arguments: FormScreenArguments( - AppLocalizations.of(context).edit, - WeightForm(currentEntry), - ), - ), - ), - PopupMenuItem( - child: Text(AppLocalizations.of(context).delete), - onTap: () async { - // Delete entry from DB - await ref - .read(bodyWeightStateProvider(auth).notifier) - .deleteEntry(currentEntry.id!); + final unit = weightUnit(profile!.isMetric, context); - // and inform the user - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - AppLocalizations.of(context).successfullyDeleted, - textAlign: TextAlign.center, - ), - ), - ); - } - }, - ), - ]; - }, - ), - ), - ); - }, + return Column( + children: [ + ...getOverviewWidgetsSeries( + AppLocalizations.of(context).weight, + entriesAll, + entries7dAvg, + plans, + unit, + context, ), - ), - ), - ], + TextButton( + onPressed: () => Navigator.pushNamed( + context, + '/measurement-categories', + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text(AppLocalizations.of(context).measurements), + const Icon(Icons.chevron_right), + ], + ), + ), + SizedBox( + height: 300, + child: RefreshIndicator( + onRefresh: () => + ref.read(bodyWeightStateProvider(auth).notifier).fetchAndSetEntries(), + child: ListView.builder( + padding: const EdgeInsets.all(10.0), + itemCount: entriesList.length, + itemBuilder: (context, index) { + final currentEntry = entriesList[index]; + return Card( + child: ListTile( + title: Text( + '${numberFormat.format(currentEntry.weight)} ${weightUnit(profile.isMetric, context)}', + ), + subtitle: Text( + DateFormat.yMd( + Localizations.localeOf(context).languageCode, + ).add_Hm().format(currentEntry.date), + ), + trailing: PopupMenuButton( + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: Text(AppLocalizations.of(context).edit), + onTap: () => Navigator.pushNamed( + context, + FormScreen.routeName, + arguments: FormScreenArguments( + AppLocalizations.of(context).edit, + WeightForm(currentEntry), + ), + ), + ), + PopupMenuItem( + child: Text(AppLocalizations.of(context).delete), + onTap: () async { + // Delete entry from DB + await ref + .read(bodyWeightStateProvider(auth).notifier) + .deleteEntry(currentEntry.id!); + + // and inform the user + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).successfullyDeleted, + textAlign: TextAlign.center, + ), + ), + ); + } + }, + ), + ]; + }, + ), + ), + ); + }, + ), + ), + ), + ], + ); + }, + loading: () { + // Show a small loading indicator while waiting for the first data set + return const Column( + children: [ + SizedBox(height: 200, child: Center(child: CircularProgressIndicator())), + ], + ); + }, + error: (err, st) { + return Column( + children: [ + Text('Error loading local weights: $err'), + Text(st.toString()), + + const SizedBox(height: 200, child: Center(child: Icon(Icons.error))), + ], + ); + }, ); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a30d39a6..b84c55f0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ 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); + g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin"); + powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) rive_common_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RivePlugin"); rive_plugin_register_with_registrar(rive_common_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index bec16405..122005a4 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + powersync_flutter_libs rive_common sqlite3_flutter_libs url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e5d0d308..20619c35 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import file_selector_macos import package_info_plus import path_provider_foundation +import powersync_flutter_libs import rive_common import shared_preferences_foundation import sqlite3_flutter_libs @@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PowersyncFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "PowersyncFlutterLibsPlugin")) RivePlugin.register(with: registry.registrar(forPlugin: "RivePlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index e6d7ec3a..bf5065bb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -828,6 +828,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.3" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" nested: dependency: transitive description: @@ -1004,6 +1012,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + powersync: + dependency: "direct main" + description: + name: powersync + sha256: "3b2694bd3b5846b445dcdfcb2b17a6dfe81e0e97d8ed531d9211478587bfda31" + url: "https://pub.dev" + source: hosted + version: "1.16.1" + powersync_core: + dependency: transitive + description: + name: powersync_core + sha256: "878f335489bffa7f92167b42ce4ac2c0895015387a74285d4f2f2a11d47e08d2" + url: "https://pub.dev" + source: hosted + version: "1.6.1" + powersync_flutter_libs: + dependency: transitive + description: + name: powersync_flutter_libs + sha256: "6385c74c3537b72086f62becd038e1f4a50bff57783e260de95f89f1c2a16aa2" + url: "https://pub.dev" + source: hosted + version: "0.4.12" process: dependency: transitive description: @@ -1185,14 +1217,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqlite3: dependency: transitive description: name: sqlite3 - sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + sha256: f18fd9a72d7a1ad2920db61368f2a69368f1cc9b56b8233e9d83b47b0a8435aa url: "https://pub.dev" source: hosted - version: "2.7.5" + version: "2.9.3" sqlite3_flutter_libs: dependency: "direct main" description: @@ -1201,6 +1241,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.40" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: "0f6ebcb4992d1892ac5c8b5ecd22a458ab9c5eb6428b11ae5ecb5d63545844da" + url: "https://pub.dev" + source: hosted + version: "0.3.2" + sqlite_async: + dependency: transitive + description: + name: sqlite_async + sha256: cf1871971324cccc03d26762ff10406fc6384af9a1ea538f12352aa5906164ec + url: "https://pub.dev" + source: hosted + version: "0.12.2" sqlparser: dependency: transitive description: @@ -1353,6 +1409,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 54d6877c..f787df30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,6 +68,7 @@ dependencies: video_player: ^2.10.0 logging: ^1.3.0 flutter_riverpod: ^2.6.1 + powersync: ^1.16.1 dev_dependencies: flutter_test: diff --git a/test/weight/weight_model_test.dart b/test/weight/weight_model_test.dart index 0bebf456..888ed260 100644 --- a/test/weight/weight_model_test.dart +++ b/test/weight/weight_model_test.dart @@ -23,19 +23,19 @@ void main() { group('fetchPost', () { test('Test that the weight entries are correctly converted to json', () { expect( - WeightEntry(id: 1, weight: 80, date: DateTime(2020, 12, 31, 12, 34)).toJson(), + WeightEntry(id: '1', weight: 80, date: DateTime(2020, 12, 31, 12, 34)).toJson(), {'id': 1, 'weight': '80', 'date': '2020-12-31T12:34:00.000'}, ); expect( - WeightEntry(id: 2, weight: 70.2, date: DateTime(2020, 12, 01)).toJson(), + WeightEntry(id: '2', weight: 70.2, date: DateTime(2020, 12, 01)).toJson(), {'id': 2, 'weight': '70.2', 'date': '2020-12-01T00:00:00.000'}, ); }); test('Test that the weight entries are correctly converted from json', () { final WeightEntry weightEntryObj = WeightEntry( - id: 1, + id: '1', weight: 80, date: DateTime(2020, 12, 31), ); @@ -53,8 +53,7 @@ void main() { group('model', () { test('Test the individual values from the model', () { WeightEntry weightModel; - //_weightModel = WeightEntry(); - weightModel = WeightEntry(id: 1, weight: 80, date: DateTime(2020, 10, 01)); + weightModel = WeightEntry(id: '1', weight: 80, date: DateTime(2020, 10, 01)); expect(weightModel.id, 1); expect(weightModel.weight, 80); diff --git a/test/weight/weight_provider_test.dart b/test/weight/weight_provider_test.dart index d45a6f79..9066a8fc 100644 --- a/test/weight/weight_provider_test.dart +++ b/test/weight/weight_provider_test.dart @@ -107,11 +107,11 @@ void main() { final BodyWeightRepository repo = BodyWeightRepository(mockBaseProvider); final BodyWeightState provider = BodyWeightState(repo); provider.state = [ - WeightEntry(id: 4, weight: 80, date: DateTime(2021, 1, 1)), - WeightEntry(id: 2, weight: 100, date: DateTime(2021, 2, 2)), - WeightEntry(id: 5, weight: 60, date: DateTime(2021, 2, 2)), + WeightEntry(id: '4', weight: 80, date: DateTime(2021, 1, 1)), + WeightEntry(id: '2', weight: 100, date: DateTime(2021, 2, 2)), + WeightEntry(id: '5', weight: 60, date: DateTime(2021, 2, 2)), ]; - await provider.deleteEntry(4); + await provider.deleteEntry('4'); // Check that the entry was removed from the entry list expect(provider.items.length, 2); diff --git a/test/weight/weight_screen_test.dart b/test/weight/weight_screen_test.dart index 59baf9e8..9e48e074 100644 --- a/test/weight/weight_screen_test.dart +++ b/test/weight/weight_screen_test.dart @@ -147,7 +147,7 @@ class FakeBodyWeightRepository extends BodyWeightRepository { FakeBodyWeightRepository(WgerBaseProvider base) : super(base); @override - Future delete(int id) async { + Future delete(String id) async { return Response('', 204); } } diff --git a/test_data/body_weight.dart b/test_data/body_weight.dart index ac806a69..fc6e50ec 100644 --- a/test_data/body_weight.dart +++ b/test_data/body_weight.dart @@ -18,8 +18,8 @@ import 'package:wger/models/body_weight/weight_entry.dart'; -final testWeightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime(2021, 01, 01, 15, 30)); -final testWeightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10, 10, 0)); +final testWeightEntry1 = WeightEntry(id: '1', weight: 80, date: DateTime(2021, 01, 01, 15, 30)); +final testWeightEntry2 = WeightEntry(id: '2', weight: 81, date: DateTime(2021, 01, 10, 10, 0)); List getWeightEntries() { return [testWeightEntry1, testWeightEntry2]; @@ -27,20 +27,20 @@ List getWeightEntries() { List getScreenshotWeightEntries() { return [ - WeightEntry(id: 1, weight: 86, date: DateTime(2021, 01, 01)), - WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10)), - WeightEntry(id: 3, weight: 82, date: DateTime(2021, 01, 20)), - WeightEntry(id: 4, weight: 83, date: DateTime(2021, 01, 30)), - WeightEntry(id: 5, weight: 86, date: DateTime(2021, 02, 20)), - WeightEntry(id: 6, weight: 90, date: DateTime(2021, 02, 28)), - WeightEntry(id: 7, weight: 91, date: DateTime(2021, 03, 20)), - WeightEntry(id: 8, weight: 91.1, date: DateTime(2021, 03, 30)), - WeightEntry(id: 9, weight: 90, date: DateTime(2021, 05, 1)), - WeightEntry(id: 10, weight: 91, date: DateTime(2021, 6, 5)), - WeightEntry(id: 11, weight: 89, date: DateTime(2021, 6, 20)), - WeightEntry(id: 12, weight: 88, date: DateTime(2021, 7, 15)), - WeightEntry(id: 13, weight: 86, date: DateTime(2021, 7, 20)), - WeightEntry(id: 14, weight: 83, date: DateTime(2021, 7, 30)), - WeightEntry(id: 15, weight: 80, date: DateTime(2021, 8, 10)), + WeightEntry(id: '1', weight: 86, date: DateTime(2021, 01, 01)), + WeightEntry(id: '2', weight: 81, date: DateTime(2021, 01, 10)), + WeightEntry(id: '3', weight: 82, date: DateTime(2021, 01, 20)), + WeightEntry(id: '4', weight: 83, date: DateTime(2021, 01, 30)), + WeightEntry(id: '5', weight: 86, date: DateTime(2021, 02, 20)), + WeightEntry(id: '6', weight: 90, date: DateTime(2021, 02, 28)), + WeightEntry(id: '7', weight: 91, date: DateTime(2021, 03, 20)), + WeightEntry(id: '8', weight: 91.1, date: DateTime(2021, 03, 30)), + WeightEntry(id: '9', weight: 90, date: DateTime(2021, 05, 1)), + WeightEntry(id: '10', weight: 91, date: DateTime(2021, 6, 5)), + WeightEntry(id: '11', weight: 89, date: DateTime(2021, 6, 20)), + WeightEntry(id: '12', weight: 88, date: DateTime(2021, 7, 15)), + WeightEntry(id: '13', weight: 86, date: DateTime(2021, 7, 20)), + WeightEntry(id: '14', weight: 83, date: DateTime(2021, 7, 30)), + WeightEntry(id: '15', weight: 80, date: DateTime(2021, 8, 10)), ]; } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3a31fc83..943344bc 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -14,6 +15,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + PowersyncFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin")); RivePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("RivePlugin")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index fa00096b..63ed6d14 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + powersync_flutter_libs rive_common sqlite3_flutter_libs url_launcher_windows