Start actually interacting with powersync

This commit is contained in:
Roland Geider
2025-10-21 12:56:55 +02:00
parent 4d2083f05a
commit 76f894ab33
24 changed files with 517 additions and 130 deletions

View File

@@ -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

65
lib/api_client.dart Normal file
View File

@@ -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<Map<String, dynamic>> 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<void> upsert(Map<String, dynamic> record) async {
await http.put(
Uri.parse('$baseUrl/api/upload-powersync-data'),
headers: {'Content-Type': 'application/json'},
body: json.encode(record),
);
}
Future<void> update(Map<String, dynamic> record) async {
await http.patch(
Uri.parse('$baseUrl/api/upload-powersync-data'),
headers: {'Content-Type': 'application/json'},
body: json.encode(record),
);
}
Future<void> delete(Map<String, dynamic> record) async {
await http.delete(
Uri.parse('$baseUrl/api/v2/upload-powersync-data'),
headers: {'Content-Type': 'application/json'},
body: json.encode(record),
);
}
}

View File

@@ -32,7 +32,9 @@ extension MeasurementChartEntryListExtensions on List<MeasurementChartEntry> {
// this also helps with computing delta's across the entire window
List<MeasurementChartEntry> 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<MeasurementChartEntry> sorted = [...this];
sorted.sort((a, b) => a.date.compareTo(b.date));
// Initialize result list
final List<MeasurementChartEntry> result = [];
@@ -46,7 +48,7 @@ extension MeasurementChartEntryListExtensions on List<MeasurementChartEntry> {
MeasurementChartEntry? lastBeforeEnd;
// Single pass through the data
for (final entry in this) {
for (final entry in sorted) {
if (entry.date.isSameDayAs(start)) {
hasEntryOnStartDay = true;
}

View File

@@ -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,

View File

@@ -9,7 +9,7 @@ part of 'weight_entry.dart';
WeightEntry _$WeightEntryFromJson(Map<String, dynamic> 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),
);

View File

@@ -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'),
],
),
]);

122
lib/powersync.dart Normal file
View File

@@ -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<RegExp> 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<PowerSyncCredentials?> 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<void> 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<String> getDatabasePath() async {
final dir = await getApplicationSupportDirectory();
return join(dir.path, 'powersync-demo.db');
}
// opens the database and connects if logged in
Future<void> 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<void> logout() async {
await db.disconnectAndClear();
}

View File

@@ -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<List<WeightEntry>> 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);
});
}

View File

@@ -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<Response> delete(int id) async {
Future<Response> 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));
}
}

View File

@@ -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<List<WeightEntry>>` that updates whenever PowerSync
/// reports changes.
final bodyWeightStreamProvider = StreamProvider.family<List<WeightEntry>, 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();
},
);

View File

@@ -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<List<WeightEntry>> {
}
}
Future<void> deleteEntry(int id) async {
Future<void> deleteEntry(String id) async {
final idx = state.indexWhere((e) => e.id == id);
if (idx < 0) {
return;

View File

@@ -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<HomeTabsScreen>
@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<HomeTabsScreen>
});
}
Future<void> _setupPowersync() async {
final authProvider = context.read<AuthProvider>();
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<HomeTabsScreen>
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

View File

@@ -40,99 +40,124 @@ class WeightOverview extends riverpod.ConsumerWidget {
final plans = context.read<NutritionPlansProvider>().items;
final auth = context.read<AuthProvider>();
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))),
],
);
},
);
}
}

View File

@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <powersync_flutter_libs/powersync_flutter_libs_plugin.h>
#include <rive_common/rive_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
@@ -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);

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
powersync_flutter_libs
rive_common
sqlite3_flutter_libs
url_launcher_linux

View File

@@ -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"))

View File

@@ -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:

View File

@@ -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:

View File

@@ -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);

View File

@@ -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);

View File

@@ -147,7 +147,7 @@ class FakeBodyWeightRepository extends BodyWeightRepository {
FakeBodyWeightRepository(WgerBaseProvider base) : super(base);
@override
Future<Response> delete(int id) async {
Future<Response> delete(String id) async {
return Response('', 204);
}
}

View File

@@ -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<WeightEntry> getWeightEntries() {
return [testWeightEntry1, testWeightEntry2];
@@ -27,20 +27,20 @@ List<WeightEntry> getWeightEntries() {
List<WeightEntry> 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)),
];
}

View File

@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <powersync_flutter_libs/powersync_flutter_libs_plugin.h>
#include <rive_common/rive_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
@@ -14,6 +15,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
PowersyncFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PowersyncFlutterLibsPlugin"));
RivePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RivePlugin"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
powersync_flutter_libs
rive_common
sqlite3_flutter_libs
url_launcher_windows