From fe48597b1608a056fd8d723b073d05edf68de0bd Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 2 Aug 2024 23:33:49 +0300 Subject: [PATCH 01/17] powersync prototype (WIP) --- lib/api_client.dart | 60 ++++++++ lib/app_config.dart | 4 + lib/main.dart | 21 +++ lib/models/schema.dart | 54 +++++++ lib/models/todo_item.dart | 50 +++++++ lib/models/todo_list.dart | 96 +++++++++++++ lib/powersync.dart | 132 ++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 82 ++++++++++- pubspec.yaml | 3 + 12 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 lib/api_client.dart create mode 100644 lib/app_config.dart create mode 100644 lib/models/schema.dart create mode 100644 lib/models/todo_item.dart create mode 100644 lib/models/todo_list.dart create mode 100644 lib/powersync.dart diff --git a/lib/api_client.dart b/lib/api_client.dart new file mode 100644 index 000000000..d6ff9c40a --- /dev/null +++ b/lib/api_client.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; + +final log = Logger('powersync-test'); + +class ApiClient { + final String baseUrl; + + ApiClient(this.baseUrl); + + Future> authenticate(String username, String password) async { + final response = await http.post( + Uri.parse('$baseUrl/api/auth/'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({'username': username, 'password': password}), + ); + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Failed to authenticate'); + } + } + + Future> getToken(String userId) async { + final response = await http.get( + Uri.parse('$baseUrl/api/get_powersync_token/'), + headers: {'Content-Type': 'application/json'}, + ); + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Failed to fetch token'); + } + } + + Future upsert(Map record) async { + await http.put( + Uri.parse('$baseUrl/api/upload_data/'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } + + Future update(Map record) async { + await http.patch( + Uri.parse('$baseUrl/api/upload_data/'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } + + Future delete(Map record) async { + await http.delete( + Uri.parse('$baseUrl/api/upload_data/'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(record), + ); + } +} diff --git a/lib/app_config.dart b/lib/app_config.dart new file mode 100644 index 000000000..e751f6945 --- /dev/null +++ b/lib/app_config.dart @@ -0,0 +1,4 @@ +class AppConfig { + static const String djangoUrl = 'http://192.168.2.223:6061'; + static const String powersyncUrl = 'http://192.168.2.223:8080'; +} diff --git a/lib/main.dart b/lib/main.dart index ac4abfe1a..ba514036f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,10 +16,12 @@ * along with this program. If not, see . */ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:wger/core/locator.dart'; +import 'package:wger/powersync.dart'; import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/base_provider.dart'; import 'package:wger/providers/body_weight.dart'; @@ -52,15 +54,34 @@ import 'package:wger/screens/workout_plans_screen.dart'; import 'package:wger/theme/theme.dart'; import 'package:wger/widgets/core/about.dart'; import 'package:wger/widgets/core/settings.dart'; +import 'package:logging/logging.dart'; import 'providers/auth.dart'; void main() async { //zx.setLogEnabled(kDebugMode); + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((record) { + if (kDebugMode) { + print('[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}'); + + if (record.error != null) { + print(record.error); + } + if (record.stackTrace != null) { + print(record.stackTrace); + } + } + }); // Needs to be called before runApp WidgetsFlutterBinding.ensureInitialized(); + await openDatabase(); + + final loggedIn = await isLoggedIn(); + print('is logged in $loggedIn'); + // Locator to initialize exerciseDB await ServiceLocator().configure(); // Application diff --git a/lib/models/schema.dart b/lib/models/schema.dart new file mode 100644 index 000000000..b85c5f103 --- /dev/null +++ b/lib/models/schema.dart @@ -0,0 +1,54 @@ +import 'package:powersync/powersync.dart'; + +const todosTable = 'todos'; + +// these are the same ones as in postgres, except for 'id' +Schema schema = const Schema(([ + Table(todosTable, [ + Column.text('list_id'), + Column.text('created_at'), + Column.text('completed_at'), + Column.text('description'), + Column.integer('completed'), + Column.text('created_by'), + Column.text('completed_by'), + ], indexes: [ + // Index to allow efficient lookup within a list + Index('list', [IndexedColumn('list_id')]) + ]), + Table('lists', + [Column.text('created_at'), Column.text('name'), Column.text('owner_id')]) +])); + +// post gres columns: +// todos: +// id | created_at | completed_at | description | completed | created_by | completed_by | list_id +// lists: +// id | created_at | name | owner_id + +// diagnostics app: +/* +new Schema([ + new Table({ + name: 'lists', // same as flutter + columns: [ + new Column({ name: 'created_at', type: ColumnType.TEXT }), + new Column({ name: 'name', type: ColumnType.TEXT }), + new Column({ name: 'owner_id', type: ColumnType.TEXT }) + ] + }), + new Table({ + name: 'todos', // misses completed_at and completed_by, until these actually get populated with something + columns: [ + new Column({ name: 'created_at', type: ColumnType.TEXT }), + new Column({ name: 'description', type: ColumnType.TEXT }), + new Column({ name: 'completed', type: ColumnType.INTEGER }), + new Column({ name: 'created_by', type: ColumnType.TEXT }), + new Column({ name: 'list_id', type: ColumnType.TEXT }) + ] + }) +]) + + Column.text('completed_at'), + Column.text('completed_by'), +*/ diff --git a/lib/models/todo_item.dart b/lib/models/todo_item.dart new file mode 100644 index 000000000..8bd10864d --- /dev/null +++ b/lib/models/todo_item.dart @@ -0,0 +1,50 @@ +import 'package:wger/models/schema.dart'; + +import '../powersync.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; + +/// TodoItem represents a result row of a query on "todos". +/// +/// This class is immutable - methods on this class do not modify the instance +/// directly. Instead, watch or re-query the data to get the updated item. +/// confirm how the watch works. this seems like a weird pattern +class TodoItem { + final String id; + final String description; + final String? photoId; + final bool completed; + + TodoItem( + {required this.id, + required this.description, + required this.completed, + required this.photoId}); + + factory TodoItem.fromRow(sqlite.Row row) { + return TodoItem( + id: row['id'], + description: row['description'], + photoId: row['photo_id'], + completed: row['completed'] == 1); + } + + Future toggle() async { + if (completed) { + await db.execute( + 'UPDATE $todosTable SET completed = FALSE, completed_by = NULL, completed_at = NULL WHERE id = ?', + [id]); + } else { + await db.execute( + 'UPDATE $todosTable SET completed = TRUE, completed_by = ?, completed_at = datetime() WHERE id = ?', + [await getUserId(), id]); + } + } + + Future delete() async { + await db.execute('DELETE FROM $todosTable WHERE id = ?', [id]); + } + + static Future addPhoto(String photoId, String id) async { + await db.execute('UPDATE $todosTable SET photo_id = ? WHERE id = ?', [photoId, id]); + } +} diff --git a/lib/models/todo_list.dart b/lib/models/todo_list.dart new file mode 100644 index 000000000..17e848b2c --- /dev/null +++ b/lib/models/todo_list.dart @@ -0,0 +1,96 @@ +import 'package:powersync/sqlite3.dart' as sqlite; + +import 'todo_item.dart'; +import '../powersync.dart'; + +/// TodoList represents a result row of a query on "lists". +/// +/// This class is immutable - methods on this class do not modify the instance +/// directly. Instead, watch or re-query the data to get the updated list. +class TodoList { + /// List id (UUID). + final String id; + + /// Descriptive name. + final String name; + + /// Number of completed todos in this list. + final int? completedCount; + + /// Number of pending todos in this list. + final int? pendingCount; + + TodoList({required this.id, required this.name, this.completedCount, this.pendingCount}); + + factory TodoList.fromRow(sqlite.Row row) { + return TodoList( + id: row['id'], + name: row['name'], + completedCount: row['completed_count'], + pendingCount: row['pending_count']); + } + + /// Watch all lists. + static Stream> watchLists() { + // This query is automatically re-run when data in "lists" or "todos" is modified. + return db.watch('SELECT * FROM lists ORDER BY created_at, id').map((results) { + return results.map(TodoList.fromRow).toList(growable: false); + }); + } + + /// Watch all lists, with [completedCount] and [pendingCount] populated. + static Stream> watchListsWithStats() { + // This query is automatically re-run when data in "lists" or "todos" is modified. + return db.watch(''' + SELECT + *, + (SELECT count() FROM todos WHERE list_id = lists.id AND completed = TRUE) as completed_count, + (SELECT count() FROM todos WHERE list_id = lists.id AND completed = FALSE) as pending_count + FROM lists + ORDER BY created_at + ''').map((results) { + return results.map(TodoList.fromRow).toList(growable: false); + }); + } + + /// Create a new list + static Future create(String name) async { + final results = await db.execute(''' + INSERT INTO + lists(id, created_at, name, owner_id) + VALUES(uuid(), datetime(), ?, ?) + RETURNING * + ''', [name, await getUserId()]); + return TodoList.fromRow(results.first); + } + + /// Watch items within this list. + Stream> watchItems() { + return db.watch('SELECT * FROM todos WHERE list_id = ? ORDER BY created_at DESC, id', + parameters: [id]).map((event) { + return event.map(TodoItem.fromRow).toList(growable: false); + }); + } + + /// Delete this list. + Future delete() async { + await db.execute('DELETE FROM lists WHERE id = ?', [id]); + } + + /// Find list item. + static Future find(id) async { + final results = await db.get('SELECT * FROM lists WHERE id = ?', [id]); + return TodoList.fromRow(results); + } + + /// Add a new todo item to this list. + Future add(String description) async { + final results = await db.execute(''' + INSERT INTO + todos(id, created_at, completed, list_id, description, created_by) + VALUES(uuid(), datetime(), FALSE, ?, ?, ?) + RETURNING * + ''', [id, description, await getUserId()]); + return TodoItem.fromRow(results.first); + } +} diff --git a/lib/powersync.dart b/lib/powersync.dart new file mode 100644 index 000000000..0eaa6c7b8 --- /dev/null +++ b/lib/powersync.dart @@ -0,0 +1,132 @@ +// 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:shared_preferences/shared_preferences.dart'; +import 'package:wger/api_client.dart'; + +import './app_config.dart'; +import './models/schema.dart'; + +final log = 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; + + DjangoConnector(this.db); + + final ApiClient apiClient = ApiClient(AppConfig.djangoUrl); + + /// Get a token to authenticate against the PowerSync instance. + @override + Future fetchCredentials() async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('id'); + if (userId == null) { + throw Exception('User does not have session'); + } + // Somewhat contrived to illustrate usage, see auth docs here: + // https://docs.powersync.com/usage/installation/authentication-setup/custom + final session = await apiClient.getToken(userId); + return PowerSyncCredentials(endpoint: AppConfig.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 (var op in transaction.crud) { + final record = { + 'table': op.table, + 'data': {'id': op.id, ...?op.opData}, + }; + + 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) { + log.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 isLoggedIn() async { + final prefs = await SharedPreferences.getInstance(); // Initialize SharedPreferences + final userId = prefs.getString('id'); + return userId != null; +} + +Future getDatabasePath() async { + final dir = await getApplicationSupportDirectory(); + return join(dir.path, 'powersync-demo.db'); +} + +// opens the database and connects if logged in +Future openDatabase() async { + // Open the local database + if (!_dbInitialized) { + db = PowerSyncDatabase(schema: schema, path: await getDatabasePath(), logger: attachedLogger); + await db.initialize(); + _dbInitialized = true; + } + + DjangoConnector? currentConnector; + + if (await isLoggedIn()) { + // If the user is already logged in, connect immediately. + // Otherwise, connect once logged in. + currentConnector = DjangoConnector(db); + db.connect(connector: currentConnector); + } +} + +/// Explicit sign out - clear database and log out. +Future logout() async { + await db.disconnectAndClear(); +} + +/// id of the user currently logged in +Future getUserId() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('id'); +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 9f99dda79..e1098f6ba 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 @@ -14,6 +15,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) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 74369f251..296e5b2f2 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 sqlite3_flutter_libs url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e5d0d3081..20619c357 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 7f1ca41d8..cf227c53d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -329,6 +329,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fetch_api: + dependency: transitive + description: + name: fetch_api + sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + fetch_client: + dependency: transitive + description: + name: fetch_client + sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" + url: "https://pub.dev" + source: hosted + version: "1.1.2" ffi: dependency: transitive description: @@ -821,7 +837,7 @@ packages: source: hosted version: "1.0.2" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" @@ -892,6 +908,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: @@ -1060,6 +1084,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + powersync: + dependency: "direct main" + description: + name: powersync + sha256: c6975007493617fdfc5945c3fab24ea2e6999ae300dd4d19d739713a4f2bcd96 + url: "https://pub.dev" + source: hosted + version: "1.6.3" + powersync_flutter_libs: + dependency: transitive + description: + name: powersync_flutter_libs + sha256: "449063aa4956c6be215ea7dfb9cc61255188e82cf7bc3f75621796fcc6615b70" + url: "https://pub.dev" + source: hosted + version: "0.1.0" process: dependency: transitive description: @@ -1233,6 +1273,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqlite3: dependency: transitive description: @@ -1249,6 +1297,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.24" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: "51fec34757577841cc72d79086067e3651c434669d5af557a5c106787198a76f" + url: "https://pub.dev" + source: hosted + version: "0.1.2-wip" + sqlite_async: + dependency: "direct main" + description: + name: sqlite_async + sha256: "79e636c857ed43f6cd5e5be72b36967a29f785daa63ff5b078bd34f74f44cb54" + url: "https://pub.dev" + source: hosted + version: "0.8.1" sqlparser: dependency: transitive description: @@ -1337,6 +1401,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: @@ -1401,6 +1473,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + url: "https://pub.dev" + source: hosted + version: "4.4.2" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3e27b5f71..1ec936fb8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: sdk: flutter android_metadata: ^0.2.1 + powersync: ^1.5.5 collection: ^1.17.0 cupertino_icons: ^1.0.8 equatable: ^2.0.5 @@ -70,6 +71,8 @@ dependencies: freezed_annotation: ^2.4.1 clock: ^1.1.1 flutter_svg_icons: ^0.0.1 + sqlite_async: ^0.8.1 + logging: ^1.2.0 dependency_overrides: intl: ^0.19.0 From 6faba1f188500cc3b3f50dfd314ce52969a20cd2 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sun, 8 Sep 2024 20:57:05 +0200 Subject: [PATCH 02/17] Fetch powersync keys --- Gemfile.lock | 34 +++++++++---------- fastlane/report.xml | 4 +-- lib/api_client.dart | 53 +++++++++++++++++++++++------- lib/app_config.dart | 4 +-- lib/models/muscle.dart | 37 +++++++++++++++++++++ lib/models/schema.dart | 51 +++++++++++++++++++--------- lib/models/todo_item.dart | 25 +++++++------- lib/models/todo_list.dart | 18 ++++++---- lib/powersync.dart | 10 ++---- lib/screens/home_tabs_screen.dart | 31 ++++++++++------- lib/widgets/dashboard/widgets.dart | 6 +++- pubspec.lock | 32 +++++++++--------- pubspec.yaml | 2 +- 13 files changed, 202 insertions(+), 105 deletions(-) create mode 100644 lib/models/muscle.dart diff --git a/Gemfile.lock b/Gemfile.lock index 86dccb02f..b4b74e3ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,25 +5,25 @@ GEM base64 nkf rexml - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.944.0) - aws-sdk-core (3.197.0) + aws-partitions (1.960.0) + aws-sdk-core (3.201.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.84.0) - aws-sdk-core (~> 3, >= 3.197.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.152.3) - aws-sdk-core (~> 3, >= 3.197.0) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.156.0) + aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,7 +38,7 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.110.0) + excon (0.111.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -60,7 +60,7 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) @@ -68,7 +68,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.220.0) + fastlane (2.222.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -153,9 +153,9 @@ GEM httpclient (2.8.3) jmespath (1.6.2) json (2.7.2) - jwt (2.8.1) + jwt (2.8.2) base64 - mini_magick (4.13.1) + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.1) @@ -165,7 +165,7 @@ GEM optparse (0.5.0) os (1.1.4) plist (3.7.1) - public_suffix (5.1.0) + public_suffix (6.0.1) rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) diff --git a/fastlane/report.xml b/fastlane/report.xml index 87898c060..31b384403 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,12 +5,12 @@ - + - + diff --git a/lib/api_client.dart b/lib/api_client.dart index d6ff9c40a..d7b0d1521 100644 --- a/lib/api_client.dart +++ b/lib/api_client.dart @@ -1,42 +1,71 @@ import 'dart:convert'; +import 'dart:io'; + import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'helpers/consts.dart'; final log = Logger('powersync-test'); class ApiClient { final String baseUrl; - ApiClient(this.baseUrl); + const ApiClient(this.baseUrl); Future> authenticate(String username, String password) async { final response = await http.post( - Uri.parse('$baseUrl/api/auth/'), + Uri.parse('$baseUrl/api/v2/login/'), headers: {'Content-Type': 'application/json'}, body: json.encode({'username': username, 'password': password}), ); if (response.statusCode == 200) { + log.log(Level.ALL, response.body); return json.decode(response.body); - } else { - throw Exception('Failed to authenticate'); } + throw Exception('Failed to authenticate'); } - Future> getToken(String userId) async { + Future> getWgerJWTToken() async { + final response = await http.post( + Uri.parse('$baseUrl/api/v2/token'), + headers: {HttpHeaders.contentTypeHeader: 'application/json'}, + body: json.encode({'username': 'admin', 'password': 'adminadmin'}), + ); + if (response.statusCode == 200) { + log.log(Level.ALL, response.body); + return json.decode(response.body); + } + throw Exception('Failed to fetch token'); + } + + /// 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 = await SharedPreferences.getInstance(); + final apiData = json.decode(prefs.getString(PREFS_USER)!); + final response = await http.get( - Uri.parse('$baseUrl/api/get_powersync_token/'), - headers: {'Content-Type': 'application/json'}, + Uri.parse('$baseUrl/api/v2/powersync-token'), + headers: { + HttpHeaders.contentTypeHeader: 'application/json', + HttpHeaders.authorizationHeader: 'Token ${apiData["token"]}', + }, ); if (response.statusCode == 200) { + log.log(Level.ALL, response.body); return json.decode(response.body); - } else { - throw Exception('Failed to fetch token'); } + throw Exception('Failed to fetch token'); } Future upsert(Map record) async { await http.put( - Uri.parse('$baseUrl/api/upload_data/'), + Uri.parse('$baseUrl/api/upload-powersync-data'), headers: {'Content-Type': 'application/json'}, body: json.encode(record), ); @@ -44,7 +73,7 @@ class ApiClient { Future update(Map record) async { await http.patch( - Uri.parse('$baseUrl/api/upload_data/'), + Uri.parse('$baseUrl/api/upload-powersync-data'), headers: {'Content-Type': 'application/json'}, body: json.encode(record), ); @@ -52,7 +81,7 @@ class ApiClient { Future delete(Map record) async { await http.delete( - Uri.parse('$baseUrl/api/upload_data/'), + Uri.parse('$baseUrl/api/v2/upload-powersync-data'), headers: {'Content-Type': 'application/json'}, body: json.encode(record), ); diff --git a/lib/app_config.dart b/lib/app_config.dart index e751f6945..78dc420ad 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -1,4 +1,4 @@ class AppConfig { - static const String djangoUrl = 'http://192.168.2.223:6061'; - static const String powersyncUrl = 'http://192.168.2.223:8080'; + static const String djangoUrl = 'http://10.0.2.2:8000'; + static const String powersyncUrl = 'http://10.0.2.2:8080'; } diff --git a/lib/models/muscle.dart b/lib/models/muscle.dart new file mode 100644 index 000000000..b910341f8 --- /dev/null +++ b/lib/models/muscle.dart @@ -0,0 +1,37 @@ +import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:wger/models/schema.dart'; +import 'package:wger/powersync.dart'; + +class Muscle { + final String id; + final String name; + final String nameEn; + final bool isFront; + + const Muscle({ + required this.id, + required this.name, + required this.nameEn, + required this.isFront, + }); + + factory Muscle.fromRow(sqlite.Row row) { + return Muscle( + id: row['id'], + name: row['name'], + nameEn: row['name_en'], + isFront: row['is_front'] == 1, + ); + } + + Future delete() async { + await db.execute('DELETE FROM $musclesTable WHERE id = ?', [id]); + } + + /// Watch all lists. + static Stream> watchMuscles() { + return db.watch('SELECT * FROM muscles ORDER BY id').map((results) { + return results.map(Muscle.fromRow).toList(growable: false); + }); + } +} diff --git a/lib/models/schema.dart b/lib/models/schema.dart index b85c5f103..b4c037847 100644 --- a/lib/models/schema.dart +++ b/lib/models/schema.dart @@ -1,24 +1,43 @@ import 'package:powersync/powersync.dart'; const todosTable = 'todos'; +const musclesTable = 'muscles'; // these are the same ones as in postgres, except for 'id' -Schema schema = const Schema(([ - Table(todosTable, [ - Column.text('list_id'), - Column.text('created_at'), - Column.text('completed_at'), - Column.text('description'), - Column.integer('completed'), - Column.text('created_by'), - Column.text('completed_by'), - ], indexes: [ - // Index to allow efficient lookup within a list - Index('list', [IndexedColumn('list_id')]) - ]), - Table('lists', - [Column.text('created_at'), Column.text('name'), Column.text('owner_id')]) -])); +Schema schema = const Schema([ + Table( + todosTable, + [ + Column.text('list_id'), + Column.text('created_at'), + Column.text('completed_at'), + Column.text('description'), + Column.integer('completed'), + Column.text('created_by'), + Column.text('completed_by'), + ], + indexes: [ + // Index to allow efficient lookup within a list + Index('list', [IndexedColumn('list_id')]), + ], + ), + Table( + 'lists', + [ + Column.text('created_at'), + Column.text('name'), + Column.text('owner_id'), + ], + ), + Table( + 'muscles', + [ + Column.text('name'), + Column.text('name_en'), + Column.text('is_front'), + ], + ), +]); // post gres columns: // todos: diff --git a/lib/models/todo_item.dart b/lib/models/todo_item.dart index 8bd10864d..1b6e6e4eb 100644 --- a/lib/models/todo_item.dart +++ b/lib/models/todo_item.dart @@ -1,7 +1,6 @@ -import 'package:wger/models/schema.dart'; - -import '../powersync.dart'; import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:wger/models/schema.dart'; +import 'package:wger/powersync.dart'; /// TodoItem represents a result row of a query on "todos". /// @@ -14,18 +13,20 @@ class TodoItem { final String? photoId; final bool completed; - TodoItem( - {required this.id, - required this.description, - required this.completed, - required this.photoId}); + TodoItem({ + required this.id, + required this.description, + required this.completed, + required this.photoId, + }); factory TodoItem.fromRow(sqlite.Row row) { return TodoItem( - id: row['id'], - description: row['description'], - photoId: row['photo_id'], - completed: row['completed'] == 1); + id: row['id'], + description: row['description'], + photoId: row['photo_id'], + completed: row['completed'] == 1, + ); } Future toggle() async { diff --git a/lib/models/todo_list.dart b/lib/models/todo_list.dart index 17e848b2c..4d965eb4c 100644 --- a/lib/models/todo_list.dart +++ b/lib/models/todo_list.dart @@ -1,7 +1,7 @@ import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:wger/powersync.dart'; import 'todo_item.dart'; -import '../powersync.dart'; /// TodoList represents a result row of a query on "lists". /// @@ -24,10 +24,11 @@ class TodoList { factory TodoList.fromRow(sqlite.Row row) { return TodoList( - id: row['id'], - name: row['name'], - completedCount: row['completed_count'], - pendingCount: row['pending_count']); + id: row['id'], + name: row['name'], + completedCount: row['completed_count'], + pendingCount: row['pending_count'], + ); } /// Watch all lists. @@ -55,12 +56,15 @@ class TodoList { /// Create a new list static Future create(String name) async { - final results = await db.execute(''' + final results = await db.execute( + ''' INSERT INTO lists(id, created_at, name, owner_id) VALUES(uuid(), datetime(), ?, ?) RETURNING * - ''', [name, await getUserId()]); + ''', + [name, await getUserId()], + ); return TodoList.fromRow(results.first); } diff --git a/lib/powersync.dart b/lib/powersync.dart index 0eaa6c7b8..bdfcdfc95 100644 --- a/lib/powersync.dart +++ b/lib/powersync.dart @@ -28,19 +28,15 @@ class DjangoConnector extends PowerSyncBackendConnector { DjangoConnector(this.db); - final ApiClient apiClient = ApiClient(AppConfig.djangoUrl); + final ApiClient apiClient = const ApiClient(AppConfig.djangoUrl); /// Get a token to authenticate against the PowerSync instance. @override Future fetchCredentials() async { - final prefs = await SharedPreferences.getInstance(); - final userId = prefs.getString('id'); - if (userId == null) { - throw Exception('User does not have session'); - } // Somewhat contrived to illustrate usage, see auth docs here: // https://docs.powersync.com/usage/installation/authentication-setup/custom - final session = await apiClient.getToken(userId); + // final wgerSession = await apiClient.getWgerJWTToken(); + final session = await apiClient.getPowersyncToken(); return PowerSyncCredentials(endpoint: AppConfig.powersyncUrl, token: session['token']); } diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index 765703be5..96faff819 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -16,13 +16,13 @@ * along with this program. If not, see . */ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:rive/rive.dart'; +import 'package:wger/powersync.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/providers/body_weight.dart'; import 'package:wger/providers/exercises.dart'; @@ -39,6 +39,7 @@ import 'package:wger/screens/workout_plans_screen.dart'; class HomeTabsScreen extends StatefulWidget { const HomeTabsScreen(); + static const routeName = '/dashboard2'; @override @@ -72,6 +73,12 @@ class _HomeTabsScreenState extends State with SingleTickerProvid /// Load initial data from the server Future _loadEntries() async { + final connector = DjangoConnector(db); + final credentials = await connector.fetchCredentials(); + print('----------'); + print(credentials); + print('----------'); + final authProvider = context.read(); if (!authProvider.dataInit) { @@ -84,7 +91,7 @@ class _HomeTabsScreenState extends State with SingleTickerProvid final userProvider = context.read(); // Base data - log('Loading base data'); + log.log(Level.FINER, Level.FINER, 'Loading base data'); try { await Future.wait([ authProvider.setServerVersion(), @@ -94,12 +101,12 @@ class _HomeTabsScreenState extends State with SingleTickerProvid exercisesProvider.fetchAndSetInitialData(), ]); } catch (e) { - log('Exception loading base data'); - log(e.toString()); + log.log(Level.FINER, 'Exception loading base data'); + log.log(Level.FINER, e.toString()); } // Plans, weight and gallery - log('Loading plans, weight, measurements and gallery'); + log.log(Level.FINER, 'Loading plans, weight, measurements and gallery'); try { await Future.wait([ galleryProvider.fetchAndSetGallery(), @@ -109,24 +116,24 @@ class _HomeTabsScreenState extends State with SingleTickerProvid measurementProvider.fetchAndSetAllCategoriesAndEntries(), ]); } catch (e) { - log('Exception loading plans, weight, measurements and gallery'); - log(e.toString()); + log.log(Level.FINER, 'Exception loading plans, weight, measurements and gallery'); + log.log(Level.FINER, e.toString()); } // Current nutritional plan - log('Loading current nutritional plan'); + log.log(Level.FINER, 'Loading current nutritional plan'); try { if (nutritionPlansProvider.currentPlan != null) { final plan = nutritionPlansProvider.currentPlan!; await nutritionPlansProvider.fetchAndSetPlanFull(plan.id!); } } catch (e) { - log('Exception loading current nutritional plan'); - log(e.toString()); + log.log(Level.FINER, 'Exception loading current nutritional plan'); + log.log(Level.FINER, e.toString()); } // Current workout plan - log('Loading current workout plan'); + log.log(Level.FINER, 'Loading current workout plan'); if (workoutPlansProvider.activePlan != null) { final planId = workoutPlansProvider.activePlan!.id!; await workoutPlansProvider.fetchAndSetWorkoutPlanFull(planId); diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index 855d6f227..65ee1c39c 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -49,6 +49,7 @@ import 'package:wger/widgets/workouts/forms.dart'; class DashboardNutritionWidget extends StatefulWidget { const DashboardNutritionWidget(); + @override _DashboardNutritionWidgetState createState() => _DashboardNutritionWidgetState(); } @@ -149,6 +150,7 @@ class _DashboardNutritionWidgetState extends State { class DashboardWeightWidget extends StatefulWidget { const DashboardWeightWidget(); + @override _DashboardWeightWidgetState createState() => _DashboardWeightWidgetState(); } @@ -235,13 +237,14 @@ class _DashboardWeightWidgetState extends State { class DashboardMeasurementWidget extends StatefulWidget { const DashboardMeasurementWidget(); + @override _DashboardMeasurementWidgetState createState() => _DashboardMeasurementWidgetState(); } class _DashboardMeasurementWidgetState extends State { int _current = 0; - final CarouselController _controller = CarouselController(); + final _controller = CarouselSliderController(); @override Widget build(BuildContext context) { @@ -346,6 +349,7 @@ class _DashboardMeasurementWidgetState extends State class DashboardWorkoutWidget extends StatefulWidget { const DashboardWorkoutWidget(); + @override _DashboardWorkoutWidgetState createState() => _DashboardWorkoutWidgetState(); } diff --git a/pubspec.lock b/pubspec.lock index cf227c53d..3a67c0793 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -173,10 +173,10 @@ packages: dependency: "direct main" description: name: carousel_slider - sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42" + sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "5.0.0" change: dependency: transitive description: @@ -800,18 +800,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -872,18 +872,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1032,10 +1032,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -1381,10 +1381,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" timing: dependency: transitive description: @@ -1573,10 +1573,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1ec936fb8..cdda63e0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: flutter_barcode_scanner: ^2.0.0 video_player: ^2.8.6 flutter_staggered_grid_view: ^0.7.0 - carousel_slider: ^4.2.1 + carousel_slider: ^5.0.0 multi_select_flutter: ^4.1.3 flutter_svg: ^2.0.10+1 fl_chart: ^0.68.0 From cea3ae15a67f5c55ab247e5ace36238f6cf6a4e4 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 10 Sep 2024 21:08:45 +0300 Subject: [PATCH 03/17] wip --- lib/models/nutrition/log_powersync.dart | 68 +++++++++++++++++++++++++ lib/models/schema.dart | 40 ++++++++++++--- lib/models/todo_item.dart | 51 ------------------- lib/powersync.dart | 20 ++++---- 4 files changed, 113 insertions(+), 66 deletions(-) create mode 100644 lib/models/nutrition/log_powersync.dart delete mode 100644 lib/models/todo_item.dart diff --git a/lib/models/nutrition/log_powersync.dart b/lib/models/nutrition/log_powersync.dart new file mode 100644 index 000000000..258191521 --- /dev/null +++ b/lib/models/nutrition/log_powersync.dart @@ -0,0 +1,68 @@ +import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:wger/models/schema.dart'; + +import '../../powersync.dart'; + +/// TodoItem represents a result row of a query on "todos". +/// +/// This class is immutable - methods on this class do not modify the instance +/// directly. Instead, watch or re-query the data to get the updated item. +/// confirm how the watch works. this seems like a weird pattern +class TodoItem { + final String id; + final String description; + final String? photoId; + final bool completed; + + TodoItem( + {required this.id, + required this.description, + required this.completed, + required this.photoId}); + + factory TodoItem.fromRow(sqlite.Row row) { + return TodoItem( + id: row['id'], + description: row['description'], + photoId: row['photo_id'], + completed: row['completed'] == 1); + } + + Future toggle() async { + if (completed) { + await db.execute( + 'UPDATE $logItemsTable SET completed = FALSE, completed_by = NULL, completed_at = NULL WHERE id = ?', + [id]); + } else { + await db.execute( + 'UPDATE $logItemsTable SET completed = TRUE, completed_by = ?, completed_at = datetime() WHERE id = ?', + [await getUserId(), id]); + } + } + + Future delete() async { + await db.execute('DELETE FROM $logItemsTable WHERE id = ?', [id]); + } + + static Future addPhoto(String photoId, String id) async { + await db.execute('UPDATE $logItemsTable SET photo_id = ? WHERE id = ?', [photoId, id]); + } +} +/* + static Stream> watchLists() { + // This query is automatically re-run when data in "lists" or "todos" is modified. + return db.watch('SELECT * FROM lists ORDER BY created_at, id').map((results) { + return results.map(TodoList.fromRow).toList(growable: false); + }); + } + + static Future create(String name) async { + final results = await db.execute(''' + INSERT INTO + lists(id, created_at, name, owner_id) + VALUES(uuid(), datetime(), ?, ?) + RETURNING * + ''', [name, await getUserId()]); + return TodoList.fromRow(results.first); + } + */ \ No newline at end of file diff --git a/lib/models/schema.dart b/lib/models/schema.dart index b4c037847..f8f68ea20 100644 --- a/lib/models/schema.dart +++ b/lib/models/schema.dart @@ -2,9 +2,41 @@ import 'package:powersync/powersync.dart'; const todosTable = 'todos'; const musclesTable = 'muscles'; +const logItemsTable = 'nutrition_logitem'; -// these are the same ones as in postgres, except for 'id' +/* +Postgres: +wger@localhost:wger> \d nutrition_logitem; ++----------------+--------------------------+--------------------------------------------+ +| Column | Type | Modifiers | +|----------------+--------------------------+--------------------------------------------| +| id | integer | not null generated by default as identity | +| datetime | timestamp with time zone | not null | +| comment | text | | +| amount | numeric(6,2) | not null | +| ingredient_id | integer | not null | +| plan_id | integer | not null | +| weight_unit_id | integer | | +| meal_id | integer | | ++----------------+--------------------------+--------------------------------------------+ +*/ +// these are the same ones as in postgres, except for 'id', because that is implied Schema schema = const Schema([ + Table( + logItemsTable, + [ + Column.text('datetime'), + Column.text('comment'), + Column.integer('amount'), + Column.integer('ingredient_id'), + Column.integer('plan_id'), + Column.integer('weight_unit_id'), + Column.integer('meal_id'), + ], + indexes: [ + // Index('plan', [IndexedColumn('plan_id')]) + ], + ), Table( todosTable, [ @@ -31,11 +63,7 @@ Schema schema = const Schema([ ), Table( 'muscles', - [ - Column.text('name'), - Column.text('name_en'), - Column.text('is_front'), - ], + [Column.text('name'), Column.text('name_en'), Column.text('is_front')], ), ]); diff --git a/lib/models/todo_item.dart b/lib/models/todo_item.dart deleted file mode 100644 index 1b6e6e4eb..000000000 --- a/lib/models/todo_item.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:powersync/sqlite3.dart' as sqlite; -import 'package:wger/models/schema.dart'; -import 'package:wger/powersync.dart'; - -/// TodoItem represents a result row of a query on "todos". -/// -/// This class is immutable - methods on this class do not modify the instance -/// directly. Instead, watch or re-query the data to get the updated item. -/// confirm how the watch works. this seems like a weird pattern -class TodoItem { - final String id; - final String description; - final String? photoId; - final bool completed; - - TodoItem({ - required this.id, - required this.description, - required this.completed, - required this.photoId, - }); - - factory TodoItem.fromRow(sqlite.Row row) { - return TodoItem( - id: row['id'], - description: row['description'], - photoId: row['photo_id'], - completed: row['completed'] == 1, - ); - } - - Future toggle() async { - if (completed) { - await db.execute( - 'UPDATE $todosTable SET completed = FALSE, completed_by = NULL, completed_at = NULL WHERE id = ?', - [id]); - } else { - await db.execute( - 'UPDATE $todosTable SET completed = TRUE, completed_by = ?, completed_at = datetime() WHERE id = ?', - [await getUserId(), id]); - } - } - - Future delete() async { - await db.execute('DELETE FROM $todosTable WHERE id = ?', [id]); - } - - static Future addPhoto(String photoId, String id) async { - await db.execute('UPDATE $todosTable SET photo_id = ? WHERE id = ?', [photoId, id]); - } -} diff --git a/lib/powersync.dart b/lib/powersync.dart index bdfcdfc95..df457aa58 100644 --- a/lib/powersync.dart +++ b/lib/powersync.dart @@ -52,7 +52,7 @@ class DjangoConnector extends PowerSyncBackendConnector { } try { - for (var op in transaction.crud) { + for (final op in transaction.crud) { final record = { 'table': op.table, 'data': {'id': op.id, ...?op.opData}, @@ -86,9 +86,14 @@ late final PowerSyncDatabase db; // Hacky flag to ensure the database is only initialized once, better to do this with listeners bool _dbInitialized = false; +/// id of the user currently logged in +Future getUserId() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('id'); +} + Future isLoggedIn() async { - final prefs = await SharedPreferences.getInstance(); // Initialize SharedPreferences - final userId = prefs.getString('id'); + final userId = await getUserId(); return userId != null; } @@ -113,6 +118,9 @@ Future openDatabase() async { // Otherwise, connect once logged in. currentConnector = DjangoConnector(db); 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 } } @@ -120,9 +128,3 @@ Future openDatabase() async { Future logout() async { await db.disconnectAndClear(); } - -/// id of the user currently logged in -Future getUserId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString('id'); -} From c3b213e2d0139f2c03d9bc3c65836a45da788045 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 10 Sep 2024 21:47:58 +0300 Subject: [PATCH 04/17] auth debug --- lib/api_client.dart | 3 ++- lib/app_config.dart | 4 ++-- lib/main.dart | 1 + lib/screens/home_tabs_screen.dart | 12 ++++++++---- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/api_client.dart b/lib/api_client.dart index d7b0d1521..0dcbefd40 100644 --- a/lib/api_client.dart +++ b/lib/api_client.dart @@ -48,7 +48,7 @@ class ApiClient { Future> getPowersyncToken() async { final prefs = await SharedPreferences.getInstance(); final apiData = json.decode(prefs.getString(PREFS_USER)!); - + print('posting our token "${apiData["token"]}" to $baseUrl/api/v2/powersync-token'); final response = await http.get( Uri.parse('$baseUrl/api/v2/powersync-token'), headers: { @@ -56,6 +56,7 @@ class ApiClient { HttpHeaders.authorizationHeader: 'Token ${apiData["token"]}', }, ); + print('response: status ${response.statusCode}, body ${response.body}'); if (response.statusCode == 200) { log.log(Level.ALL, response.body); return json.decode(response.body); diff --git a/lib/app_config.dart b/lib/app_config.dart index 78dc420ad..49d9477bf 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -1,4 +1,4 @@ class AppConfig { - static const String djangoUrl = 'http://10.0.2.2:8000'; - static const String powersyncUrl = 'http://10.0.2.2:8080'; + static const String djangoUrl = 'http://192.168.1.195:8000'; + static const String powersyncUrl = 'http://192.168.1.195:8080'; } diff --git a/lib/main.dart b/lib/main.dart index ba514036f..15430ed14 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -84,6 +84,7 @@ void main() async { // Locator to initialize exerciseDB await ServiceLocator().configure(); + print('running myapp'); // Application runApp(const MyApp()); } diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index 96faff819..127bfe136 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -74,10 +74,14 @@ class _HomeTabsScreenState extends State with SingleTickerProvid /// Load initial data from the server Future _loadEntries() async { final connector = DjangoConnector(db); - final credentials = await connector.fetchCredentials(); - print('----------'); - print(credentials); - print('----------'); + try { + final credentials = await connector.fetchCredentials(); + print('----------'); + print(credentials); + print('----------'); + } catch (e) { + print('fail' + e.toString()); + } final authProvider = context.read(); From a1295e564918c5545983d2e1467b29baea093dbf Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 10 Sep 2024 22:06:24 +0300 Subject: [PATCH 05/17] wip logitem --- lib/models/nutrition/log.dart | 43 ++++++++++++++++ lib/models/nutrition/log_powersync.dart | 68 ------------------------- 2 files changed, 43 insertions(+), 68 deletions(-) delete mode 100644 lib/models/nutrition/log_powersync.dart diff --git a/lib/models/nutrition/log.dart b/lib/models/nutrition/log.dart index ac4aa8457..8799d6ce0 100644 --- a/lib/models/nutrition/log.dart +++ b/lib/models/nutrition/log.dart @@ -17,11 +17,14 @@ */ import 'package:json_annotation/json_annotation.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; import 'package:wger/helpers/json.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/ingredient_weight_unit.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; +import 'package:wger/models/schema.dart'; +import 'package:wger/powersync.dart'; part 'log.g.dart'; @@ -75,6 +78,19 @@ class Log { amount = mealItem.amount; } + factory Log.fromRow(sqlite.Row row) { + return Log( + id: row['id'], + mealId: row['meal_id'], + ingredientId: row['ingredient_id'], + weightUnitId: row['weight_unit_id'], + amount: row['amount'], + planId: row['plan_id'], + datetime: row['datetime'], + comment: row['comment'], + ); + } + // Boilerplate factory Log.fromJson(Map json) => _$LogFromJson(json); @@ -89,4 +105,31 @@ class Log { return ingredient.nutritionalValues / (100 / weight); } +/* + Future delete() async { + await db.execute('DELETE FROM $logItemsTable WHERE id = ?', [id]); + } + + static Future addPhoto(String photoId, String id) async { + await db.execute('UPDATE $logItemsTable SET photo_id = ? WHERE id = ?', [photoId, id]); + } +} + + static Stream> watchLists() { + // This query is automatically re-run when data in "lists" or "todos" is modified. + return db.watch('SELECT * FROM lists ORDER BY created_at, id').map((results) { + return results.map(TodoList.fromRow).toList(growable: false); + }); + } + + static Future create(String name) async { + final results = await db.execute(''' + INSERT INTO + lists(id, created_at, name, owner_id) + VALUES(uuid(), datetime(), ?, ?) + RETURNING * + ''', [name, await getUserId()]); + return TodoList.fromRow(results.first); + } + */ } diff --git a/lib/models/nutrition/log_powersync.dart b/lib/models/nutrition/log_powersync.dart deleted file mode 100644 index 258191521..000000000 --- a/lib/models/nutrition/log_powersync.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:powersync/sqlite3.dart' as sqlite; -import 'package:wger/models/schema.dart'; - -import '../../powersync.dart'; - -/// TodoItem represents a result row of a query on "todos". -/// -/// This class is immutable - methods on this class do not modify the instance -/// directly. Instead, watch or re-query the data to get the updated item. -/// confirm how the watch works. this seems like a weird pattern -class TodoItem { - final String id; - final String description; - final String? photoId; - final bool completed; - - TodoItem( - {required this.id, - required this.description, - required this.completed, - required this.photoId}); - - factory TodoItem.fromRow(sqlite.Row row) { - return TodoItem( - id: row['id'], - description: row['description'], - photoId: row['photo_id'], - completed: row['completed'] == 1); - } - - Future toggle() async { - if (completed) { - await db.execute( - 'UPDATE $logItemsTable SET completed = FALSE, completed_by = NULL, completed_at = NULL WHERE id = ?', - [id]); - } else { - await db.execute( - 'UPDATE $logItemsTable SET completed = TRUE, completed_by = ?, completed_at = datetime() WHERE id = ?', - [await getUserId(), id]); - } - } - - Future delete() async { - await db.execute('DELETE FROM $logItemsTable WHERE id = ?', [id]); - } - - static Future addPhoto(String photoId, String id) async { - await db.execute('UPDATE $logItemsTable SET photo_id = ? WHERE id = ?', [photoId, id]); - } -} -/* - static Stream> watchLists() { - // This query is automatically re-run when data in "lists" or "todos" is modified. - return db.watch('SELECT * FROM lists ORDER BY created_at, id').map((results) { - return results.map(TodoList.fromRow).toList(growable: false); - }); - } - - static Future create(String name) async { - final results = await db.execute(''' - INSERT INTO - lists(id, created_at, name, owner_id) - VALUES(uuid(), datetime(), ?, ?) - RETURNING * - ''', [name, await getUserId()]); - return TodoList.fromRow(results.first); - } - */ \ No newline at end of file From 9332bb1bb2a317d44268be7b9808b8bee1610550 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Fri, 13 Sep 2024 21:48:21 +0300 Subject: [PATCH 06/17] wip --- lib/api_client.dart | 2 +- lib/app_config.dart | 1 + lib/main.dart | 2 +- lib/screens/home_tabs_screen.dart | 7 ++++++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/api_client.dart b/lib/api_client.dart index 0dcbefd40..b882cd1d0 100644 --- a/lib/api_client.dart +++ b/lib/api_client.dart @@ -31,7 +31,7 @@ class ApiClient { final response = await http.post( Uri.parse('$baseUrl/api/v2/token'), headers: {HttpHeaders.contentTypeHeader: 'application/json'}, - body: json.encode({'username': 'admin', 'password': 'adminadmin'}), + body: json.encode({'username': 'admin', 'password': 'adminadmin'}), // FIXME ); if (response.statusCode == 200) { log.log(Level.ALL, response.body); diff --git a/lib/app_config.dart b/lib/app_config.dart index 49d9477bf..58d87fdd0 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -1,4 +1,5 @@ class AppConfig { + // TODO: base this off of base URL returned by auth provider static const String djangoUrl = 'http://192.168.1.195:8000'; static const String powersyncUrl = 'http://192.168.1.195:8080'; } diff --git a/lib/main.dart b/lib/main.dart index 15430ed14..f58bc5498 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -80,7 +80,7 @@ void main() async { await openDatabase(); final loggedIn = await isLoggedIn(); - print('is logged in $loggedIn'); + print('main(): is logged in $loggedIn'); // Locator to initialize exerciseDB await ServiceLocator().configure(); diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index 127bfe136..11ca031b7 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -53,6 +53,10 @@ class _HomeTabsScreenState extends State with SingleTickerProvid @override void initState() { super.initState(); + + final authProvider = context.read(); + print('auth provider says surverurl is ${authProvider.serverUrl}'); + // Loading data here, since the build method can be called more than once _initialData = _loadEntries(); } @@ -82,7 +86,8 @@ class _HomeTabsScreenState extends State with SingleTickerProvid } catch (e) { print('fail' + e.toString()); } - + final loggedIn = await isLoggedIn(); + print('_loadEntries(): is logged in $loggedIn'); final authProvider = context.read(); if (!authProvider.dataInit) { From bb679754b9eea0cfdd0fe24d88b2c0195f5e872e Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sat, 14 Sep 2024 13:22:10 +0300 Subject: [PATCH 07/17] fix/refactor/simplify powersync auth setup --- lib/api_client.dart | 26 -------------------------- lib/main.dart | 5 ----- lib/powersync.dart | 21 ++++----------------- lib/providers/auth.dart | 1 + lib/screens/home_tabs_screen.dart | 20 ++++++++++++++------ 5 files changed, 19 insertions(+), 54 deletions(-) diff --git a/lib/api_client.dart b/lib/api_client.dart index b882cd1d0..bde351c5c 100644 --- a/lib/api_client.dart +++ b/lib/api_client.dart @@ -14,32 +14,6 @@ class ApiClient { const ApiClient(this.baseUrl); - Future> authenticate(String username, String password) async { - final response = await http.post( - Uri.parse('$baseUrl/api/v2/login/'), - headers: {'Content-Type': 'application/json'}, - body: json.encode({'username': username, 'password': password}), - ); - if (response.statusCode == 200) { - log.log(Level.ALL, response.body); - return json.decode(response.body); - } - throw Exception('Failed to authenticate'); - } - - Future> getWgerJWTToken() async { - final response = await http.post( - Uri.parse('$baseUrl/api/v2/token'), - headers: {HttpHeaders.contentTypeHeader: 'application/json'}, - body: json.encode({'username': 'admin', 'password': 'adminadmin'}), // FIXME - ); - if (response.statusCode == 200) { - log.log(Level.ALL, response.body); - return json.decode(response.body); - } - throw Exception('Failed to fetch token'); - } - /// Returns a powersync JWT token token /// /// Note that at the moment we use the permanent API token for authentication diff --git a/lib/main.dart b/lib/main.dart index f58bc5498..a7827d918 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -77,11 +77,6 @@ void main() async { // Needs to be called before runApp WidgetsFlutterBinding.ensureInitialized(); - await openDatabase(); - - final loggedIn = await isLoggedIn(); - print('main(): is logged in $loggedIn'); - // Locator to initialize exerciseDB await ServiceLocator().configure(); print('running myapp'); diff --git a/lib/powersync.dart b/lib/powersync.dart index df457aa58..5bb7e1b9c 100644 --- a/lib/powersync.dart +++ b/lib/powersync.dart @@ -3,7 +3,6 @@ import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:powersync/powersync.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:wger/api_client.dart'; import './app_config.dart'; @@ -37,6 +36,7 @@ class DjangoConnector extends PowerSyncBackendConnector { // 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 return PowerSyncCredentials(endpoint: AppConfig.powersyncUrl, token: session['token']); } @@ -86,24 +86,13 @@ late final PowerSyncDatabase db; // Hacky flag to ensure the database is only initialized once, better to do this with listeners bool _dbInitialized = false; -/// id of the user currently logged in -Future getUserId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString('id'); -} - -Future isLoggedIn() async { - final userId = await getUserId(); - return userId != null; -} - Future getDatabasePath() async { final dir = await getApplicationSupportDirectory(); return join(dir.path, 'powersync-demo.db'); } // opens the database and connects if logged in -Future openDatabase() async { +Future openDatabase(bool connect) async { // Open the local database if (!_dbInitialized) { db = PowerSyncDatabase(schema: schema, path: await getDatabasePath(), logger: attachedLogger); @@ -111,12 +100,10 @@ Future openDatabase() async { _dbInitialized = true; } - DjangoConnector? currentConnector; - - if (await isLoggedIn()) { + if (connect) { // If the user is already logged in, connect immediately. // Otherwise, connect once logged in. - currentConnector = DjangoConnector(db); + final currentConnector = DjangoConnector(db); db.connect(connector: currentConnector); // TODO: should we respond to login state changing? like here: diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 61e25617d..fa07f0d9f 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -182,6 +182,7 @@ class AuthProvider with ChangeNotifier { } // Log user in + // should we update the backend to just include a powersync token also? token = responseData['token']; notifyListeners(); diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index 11ca031b7..8c0f96709 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -54,8 +54,8 @@ class _HomeTabsScreenState extends State with SingleTickerProvid void initState() { super.initState(); - final authProvider = context.read(); - print('auth provider says surverurl is ${authProvider.serverUrl}'); + // 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(); @@ -75,19 +75,27 @@ class _HomeTabsScreenState extends State with SingleTickerProvid const GalleryScreen(), ]; - /// Load initial data from the server - Future _loadEntries() async { + Future _setupPowersync() async { + final authProvider = context.read(); + print('auth provider says surverurl is ${authProvider.serverUrl}'); + await openDatabase(false); + final connector = DjangoConnector(db); 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(); print('----------'); print(credentials); print('----------'); + await openDatabase(true); } catch (e) { print('fail' + e.toString()); } - final loggedIn = await isLoggedIn(); - print('_loadEntries(): is logged in $loggedIn'); + } + + /// Load initial data from the server + Future _loadEntries() async { final authProvider = context.read(); if (!authProvider.dataInit) { From 770ea64a23767ab6df6d90693b602db22faf1f11 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 15 Sep 2024 12:25:54 +0300 Subject: [PATCH 08/17] fix tablename --- lib/models/muscle.dart | 2 +- lib/models/schema.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/muscle.dart b/lib/models/muscle.dart index b910341f8..d76238fe5 100644 --- a/lib/models/muscle.dart +++ b/lib/models/muscle.dart @@ -30,7 +30,7 @@ class Muscle { /// Watch all lists. static Stream> watchMuscles() { - return db.watch('SELECT * FROM muscles ORDER BY id').map((results) { + return db.watch('SELECT * FROM $musclesTable ORDER BY id').map((results) { return results.map(Muscle.fromRow).toList(growable: false); }); } diff --git a/lib/models/schema.dart b/lib/models/schema.dart index f8f68ea20..6bc8e8083 100644 --- a/lib/models/schema.dart +++ b/lib/models/schema.dart @@ -1,7 +1,7 @@ import 'package:powersync/powersync.dart'; const todosTable = 'todos'; -const musclesTable = 'muscles'; +const musclesTable = 'exercises_muscle'; const logItemsTable = 'nutrition_logitem'; /* @@ -62,7 +62,7 @@ Schema schema = const Schema([ ], ), Table( - 'muscles', + 'exercises_muscle', [Column.text('name'), Column.text('name_en'), Column.text('is_front')], ), ]); From 4fef0b052d37107ac6c74acb3d675b7be4c54629 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 15 Sep 2024 12:26:33 +0300 Subject: [PATCH 09/17] figure out django/powersync url's from authProvider --- lib/app_config.dart | 5 ----- lib/powersync.dart | 17 ++++++++++------- lib/screens/home_tabs_screen.dart | 10 ++++++---- 3 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 lib/app_config.dart diff --git a/lib/app_config.dart b/lib/app_config.dart deleted file mode 100644 index 58d87fdd0..000000000 --- a/lib/app_config.dart +++ /dev/null @@ -1,5 +0,0 @@ -class AppConfig { - // TODO: base this off of base URL returned by auth provider - static const String djangoUrl = 'http://192.168.1.195:8000'; - static const String powersyncUrl = 'http://192.168.1.195:8080'; -} diff --git a/lib/powersync.dart b/lib/powersync.dart index 5bb7e1b9c..4532ae0f7 100644 --- a/lib/powersync.dart +++ b/lib/powersync.dart @@ -5,7 +5,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:powersync/powersync.dart'; import 'package:wger/api_client.dart'; -import './app_config.dart'; import './models/schema.dart'; final log = Logger('powersync-django'); @@ -24,10 +23,13 @@ final List fatalResponseCodes = [ class DjangoConnector extends PowerSyncBackendConnector { PowerSyncDatabase db; + String baseUrl; + String powersyncUrl; + late ApiClient apiClient; - DjangoConnector(this.db); - - final ApiClient apiClient = const ApiClient(AppConfig.djangoUrl); + DjangoConnector(this.db, this.baseUrl, this.powersyncUrl) { + apiClient = ApiClient(baseUrl); + } /// Get a token to authenticate against the PowerSync instance. @override @@ -37,7 +39,7 @@ class DjangoConnector extends PowerSyncBackendConnector { // final wgerSession = await apiClient.getWgerJWTToken(); final session = await apiClient.getPowersyncToken(); // note: we don't set userId and expires property here. not sure if needed - return PowerSyncCredentials(endpoint: AppConfig.powersyncUrl, token: session['token']); + return PowerSyncCredentials(endpoint: this.powersyncUrl, token: session['token']); } // Upload pending changes to Postgres via Django backend @@ -92,7 +94,7 @@ Future getDatabasePath() async { } // opens the database and connects if logged in -Future openDatabase(bool connect) async { +Future openDatabase(bool connect, String baseUrl, String powersyncUrl) async { // Open the local database if (!_dbInitialized) { db = PowerSyncDatabase(schema: schema, path: await getDatabasePath(), logger: attachedLogger); @@ -103,7 +105,8 @@ Future openDatabase(bool connect) async { if (connect) { // If the user is already logged in, connect immediately. // Otherwise, connect once logged in. - final currentConnector = DjangoConnector(db); + + final currentConnector = DjangoConnector(db, baseUrl, powersyncUrl); db.connect(connector: currentConnector); // TODO: should we respond to login state changing? like here: diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index 8c0f96709..b5a29b05a 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -77,10 +77,12 @@ class _HomeTabsScreenState extends State with SingleTickerProvid Future _setupPowersync() async { final authProvider = context.read(); - print('auth provider says surverurl is ${authProvider.serverUrl}'); - await openDatabase(false); + final baseUrl = authProvider.serverUrl!; + final powerSyncUrl = baseUrl.replaceAll(':8000', ':8080'); - final connector = DjangoConnector(db); + 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 @@ -88,7 +90,7 @@ class _HomeTabsScreenState extends State with SingleTickerProvid print('----------'); print(credentials); print('----------'); - await openDatabase(true); + await openDatabase(true, baseUrl, powerSyncUrl); } catch (e) { print('fail' + e.toString()); } From e02bf6b31fffc0accd838b14870dfe85b072b264 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 15 Sep 2024 19:37:21 +0300 Subject: [PATCH 10/17] powersync based muscles --- lib/models/muscle.dart | 5 +- lib/models/schema.dart | 112 ++++++++++-------------------- lib/providers/nutrition.dart | 3 + lib/screens/dashboard.dart | 47 +++++++++++++ lib/screens/home_tabs_screen.dart | 6 +- 5 files changed, 92 insertions(+), 81 deletions(-) diff --git a/lib/models/muscle.dart b/lib/models/muscle.dart index d76238fe5..1b5b9788b 100644 --- a/lib/models/muscle.dart +++ b/lib/models/muscle.dart @@ -25,12 +25,13 @@ class Muscle { } Future delete() async { - await db.execute('DELETE FROM $musclesTable WHERE id = ?', [id]); + await db.execute('DELETE FROM $tableMuscles WHERE id = ?', [id]); } /// Watch all lists. static Stream> watchMuscles() { - return db.watch('SELECT * FROM $musclesTable ORDER BY id').map((results) { + return db.watch('SELECT * FROM $tableMuscles ORDER BY id').map((results) { + print('watchMuscles triggered' + results.toString()); return results.map(Muscle.fromRow).toList(growable: false); }); } diff --git a/lib/models/schema.dart b/lib/models/schema.dart index 6bc8e8083..b964c4c61 100644 --- a/lib/models/schema.dart +++ b/lib/models/schema.dart @@ -1,29 +1,33 @@ import 'package:powersync/powersync.dart'; -const todosTable = 'todos'; -const musclesTable = 'exercises_muscle'; -const logItemsTable = 'nutrition_logitem'; +const tableMuscles = 'exercises_muscle'; +const tableLogItems = 'nutrition_logitem'; +const tableNutritionPlans = 'nutrition_nutritionplan'; +const tableMeals = 'nutrition_meal'; +const tableMealItems = 'nutrition_mealitem'; -/* -Postgres: -wger@localhost:wger> \d nutrition_logitem; -+----------------+--------------------------+--------------------------------------------+ -| Column | Type | Modifiers | -|----------------+--------------------------+--------------------------------------------| -| id | integer | not null generated by default as identity | -| datetime | timestamp with time zone | not null | -| comment | text | | -| amount | numeric(6,2) | not null | -| ingredient_id | integer | not null | -| plan_id | integer | not null | -| weight_unit_id | integer | | -| meal_id | integer | | -+----------------+--------------------------+--------------------------------------------+ -*/ -// these are the same ones as in postgres, except for 'id', because that is implied Schema schema = const Schema([ Table( - logItemsTable, + tableMuscles, + [Column.text('name'), Column.text('name_en'), Column.text('is_front')], + ), + Table( + tableNutritionPlans, + [ + Column.text('creation_date'), + Column.text('description'), + Column.integer('has_goal_calories'), + Column.integer('user_id'), + Column.integer('only_logging'), + Column.integer('goal_carbohydrates'), + Column.integer('goal_energy'), + Column.integer('goal_fat'), + Column.integer('goal_protein'), + Column.integer('goal_fiber'), + ], + ), + Table( + tableLogItems, [ Column.text('datetime'), Column.text('comment'), @@ -31,71 +35,29 @@ Schema schema = const Schema([ Column.integer('ingredient_id'), Column.integer('plan_id'), Column.integer('weight_unit_id'), - Column.integer('meal_id'), + Column.integer('meal_id'), // optional ], indexes: [ // Index('plan', [IndexedColumn('plan_id')]) ], ), Table( - todosTable, + tableMeals, [ - Column.text('list_id'), - Column.text('created_at'), - Column.text('completed_at'), - Column.text('description'), - Column.integer('completed'), - Column.text('created_by'), - Column.text('completed_by'), - ], - indexes: [ - // Index to allow efficient lookup within a list - Index('list', [IndexedColumn('list_id')]), + Column.integer('order'), + Column.text('time'), + Column.integer('plan_id'), + Column.text('name'), ], ), Table( - 'lists', + tableMealItems, [ - Column.text('created_at'), - Column.text('name'), - Column.text('owner_id'), + Column.integer('order'), + Column.integer('amount'), + Column.integer('ingredient_id'), + Column.integer('meal_id'), + Column.integer('weight_unit_id'), ], ), - Table( - 'exercises_muscle', - [Column.text('name'), Column.text('name_en'), Column.text('is_front')], - ), ]); - -// post gres columns: -// todos: -// id | created_at | completed_at | description | completed | created_by | completed_by | list_id -// lists: -// id | created_at | name | owner_id - -// diagnostics app: -/* -new Schema([ - new Table({ - name: 'lists', // same as flutter - columns: [ - new Column({ name: 'created_at', type: ColumnType.TEXT }), - new Column({ name: 'name', type: ColumnType.TEXT }), - new Column({ name: 'owner_id', type: ColumnType.TEXT }) - ] - }), - new Table({ - name: 'todos', // misses completed_at and completed_by, until these actually get populated with something - columns: [ - new Column({ name: 'created_at', type: ColumnType.TEXT }), - new Column({ name: 'description', type: ColumnType.TEXT }), - new Column({ name: 'completed', type: ColumnType.INTEGER }), - new Column({ name: 'created_by', type: ColumnType.TEXT }), - new Column({ name: 'list_id', type: ColumnType.TEXT }) - ] - }) -]) - - Column.text('completed_at'), - Column.text('completed_by'), -*/ diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 9c88eee72..6a5e11c6d 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -125,6 +125,9 @@ class NutritionPlansProvider with ChangeNotifier { } /// Fetches a plan fully, i.e. with all corresponding child objects + /// + + Future fetchAndSetPlanFull(int planId) async { NutritionalPlan plan; try { diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 02d524f3c..43a0fbde6 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -16,8 +16,11 @@ * along with this program. If not, see . */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:wger/models/muscle.dart'; import 'package:wger/widgets/core/app_bar.dart'; import 'package:wger/widgets/dashboard/calendar.dart'; import 'package:wger/widgets/dashboard/widgets.dart'; @@ -39,6 +42,7 @@ class _DashboardScreenState extends State { padding: EdgeInsets.all(10), child: Column( children: [ + DashboardMuscleWidget(), DashboardWorkoutWidget(), DashboardNutritionWidget(), DashboardWeightWidget(), @@ -50,3 +54,46 @@ class _DashboardScreenState extends State { ); } } + +class DashboardMuscleWidget extends StatefulWidget { + const DashboardMuscleWidget({super.key}); + + @override + _DashboardMuscleWidgetState createState() => _DashboardMuscleWidgetState(); +} + +class _DashboardMuscleWidgetState extends State { + List _data = []; + StreamSubscription? _subscription; + + _DashboardMuscleWidgetState(); + + @override + void initState() { + super.initState(); + final stream = Muscle.watchMuscles(); + _subscription = stream.listen((data) { + if (!context.mounted) { + return; + } + setState(() { + _data = data; + }); + }); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.brown, + child: Column( + children: [Text('muscles'), ..._data.map((e) => Text(e.name)).toList()], + )); + } +} diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index b5a29b05a..0d026ede5 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -87,12 +87,10 @@ class _HomeTabsScreenState extends State with SingleTickerProvid // 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(); - print('----------'); - print(credentials); - print('----------'); + print('fetched credentials' + credentials.toString()); await openDatabase(true, baseUrl, powerSyncUrl); } catch (e) { - print('fail' + e.toString()); + print('failed to fetchCredentials()' + e.toString()); } } From 96b164f612edfdc4a6a1391042979fbb4a7f54f1 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 15 Sep 2024 19:37:44 +0300 Subject: [PATCH 11/17] powersync based nutrition --- lib/models/muscle.dart | 1 - lib/models/nutrition/log.dart | 19 ++- lib/models/nutrition/meal.dart | 22 +++ lib/models/nutrition/nutritional_plan.dart | 113 +++++++++++++ lib/models/todo_list.dart | 100 ------------ lib/providers/nutrition.dart | 53 +------ lib/screens/home_tabs_screen.dart | 15 +- lib/screens/nutritional_plan_screen.dart | 6 +- lib/widgets/dashboard/widgets.dart | 22 ++- .../nutrition/nutritional_plans_list.dart | 148 +++++++++--------- 10 files changed, 245 insertions(+), 254 deletions(-) delete mode 100644 lib/models/todo_list.dart diff --git a/lib/models/muscle.dart b/lib/models/muscle.dart index 1b5b9788b..2c86437e5 100644 --- a/lib/models/muscle.dart +++ b/lib/models/muscle.dart @@ -31,7 +31,6 @@ class Muscle { /// Watch all lists. static Stream> watchMuscles() { return db.watch('SELECT * FROM $tableMuscles ORDER BY id').map((results) { - print('watchMuscles triggered' + results.toString()); return results.map(Muscle.fromRow).toList(growable: false); }); } diff --git a/lib/models/nutrition/log.dart b/lib/models/nutrition/log.dart index 8799d6ce0..5b56fe8b1 100644 --- a/lib/models/nutrition/log.dart +++ b/lib/models/nutrition/log.dart @@ -25,6 +25,7 @@ import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; import 'package:wger/models/schema.dart'; import 'package:wger/powersync.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; part 'log.g.dart'; @@ -80,13 +81,13 @@ class Log { factory Log.fromRow(sqlite.Row row) { return Log( - id: row['id'], - mealId: row['meal_id'], - ingredientId: row['ingredient_id'], - weightUnitId: row['weight_unit_id'], + id: int.parse(row['id']), + mealId: int.parse(row['meal_id']), + ingredientId: int.parse(row['ingredient_id']), + weightUnitId: int.parse(row['weight_unit_id']), amount: row['amount'], - planId: row['plan_id'], - datetime: row['datetime'], + planId: int.parse(row['plan_id']), + datetime: DateTime.parse(row['datetime']), comment: row['comment'], ); } @@ -105,6 +106,12 @@ class Log { return ingredient.nutritionalValues / (100 / weight); } + + static Future> readByPlanId(int planId) async { + final results = await db.getAll('SELECT * FROM $tableLogItems WHERE plan_id = ?', [planId]); + return results.map((r) => Log.fromRow(r)).toList(); + } + /* Future delete() async { await db.execute('DELETE FROM $logItemsTable WHERE id = ?', [id]); diff --git a/lib/models/nutrition/meal.dart b/lib/models/nutrition/meal.dart index ecb3ca3f5..ea7080f8c 100644 --- a/lib/models/nutrition/meal.dart +++ b/lib/models/nutrition/meal.dart @@ -24,6 +24,9 @@ import 'package:wger/helpers/misc.dart'; import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:wger/models/schema.dart'; +import 'package:wger/powersync.dart'; part 'meal.g.dart'; @@ -87,6 +90,15 @@ class Meal { // Boilerplate factory Meal.fromJson(Map json) => _$MealFromJson(json); + factory Meal.fromRow(sqlite.Row row) { + return Meal( + id: int.parse(row['id']), + plan: int.parse(row['plan']), + time: stringToTime(row['time']), + name: row['name'], + ); + } + Map toJson() => _$MealToJson(this); Meal copyWith({ @@ -106,4 +118,14 @@ class Meal { diaryEntries: diaryEntries ?? this.diaryEntries, ); } + + static Future read(int id) async { + final results = await db.get('SELECT * FROM $tableMeals WHERE id = ?', [id]); + return Meal.fromRow(results); + } + + static Future> readByPlanId(int planId) async { + final results = await db.getAll('SELECT * FROM $tableMeals WHERE plan_id = ?', [planId]); + return results.map((r) => Meal.fromRow(r)).toList(); + } } diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index a0f678397..fe635aa19 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -20,6 +20,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/models/nutrition/log.dart'; @@ -27,6 +28,8 @@ import 'package:wger/models/nutrition/meal.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_goals.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; +import 'package:wger/models/schema.dart'; +import 'package:wger/powersync.dart'; part 'nutritional_plan.g.dart'; @@ -82,6 +85,55 @@ class NutritionalPlan { this.diaryEntries = diaryEntries ?? []; } + factory NutritionalPlan.fromRow(sqlite.Row row) { + return NutritionalPlan( + id: int.parse(row['id']), + description: row['description'], + creationDate: DateTime.parse(row['creation_date']), + onlyLogging: row['only_logging'] == 1, + goalEnergy: row['goal_energy'], + goalProtein: row['goal_protein'], + goalCarbohydrates: row['goal_carbohydrates'], + goalFat: row['goal_fat'], + goalFiber: row['goal_fiber'], + ); + } + + NutritionalPlan copyWith({ + int? id, + String? description, + DateTime? creationDate, + bool? onlyLogging, + num? goalEnergy, + num? goalProtein, + num? goalCarbohydrates, + num? goalFat, + num? goalFiber, + List? meals, + List? diaryEntries, + }) { + return NutritionalPlan( + id: id ?? this.id, + description: description ?? this.description, + creationDate: creationDate ?? this.creationDate, + onlyLogging: onlyLogging ?? this.onlyLogging, + goalEnergy: goalEnergy ?? this.goalEnergy, + goalProtein: goalProtein ?? this.goalProtein, + goalCarbohydrates: goalCarbohydrates ?? this.goalCarbohydrates, + goalFat: goalFat ?? this.goalFat, + goalFiber: goalFiber ?? this.goalFiber, + meals: meals ?? this.meals, + diaryEntries: diaryEntries ?? this.diaryEntries, + ); + } + + Future loadChildren() async { + return copyWith( + diaryEntries: await Log.readByPlanId(id!), + meals: await Meal.readByPlanId(id!), + ); + } + NutritionalPlan.empty() { creationDate = DateTime.now(); description = ''; @@ -246,4 +298,65 @@ class NutritionalPlan { diaryEntries: diaryEntries.where((e) => e.mealId == null).toList(), ); } + + static Future read(int id) async { + final row = await db.get('SELECT * FROM $tableNutritionPlans WHERE id = ?', [id]); + return NutritionalPlan.fromRow(row).loadChildren(); + } + +// this is a bit complicated. +// what we need at the end of the day, is a stream of List, where +// a new value is emitted any time a plan is changed. But the plan is not just the plan record +// we need to load data for Logs and Meals corresponding to the plan also. +// so our options are: +// 1) db.watch with a select query on plans; and extra dart code to load the logs/meals stuff, +// but this only triggers for updates on the plans table, and misses logs/meals updates +// 2) db.watch with a huge join query across all tables from which we need info, +// so we have all the data in our resultset to create the datastructures with, but: +// - this creates long rows with lots of duplicated data (e.g. all the plan data) for every row +// which would only differ for e.g. the meal or the log item +// - it would probably get a bit messy to parse the resultset into the datastructures +// 3) the best of both worlds: load the data we need in dart at runtime, but explicitly +// trigger our code execution when *any* of the relevant tables changes +// + static Stream> watchNutritionPlans() { + return db.onChange([tableNutritionPlans, tableLogItems, tableMeals]).asyncMap((event) async { + final data = await db.getAll('SELECT * FROM $tableNutritionPlans ORDER BY creation_date'); + final futures = Future.wait(data.map((row) => NutritionalPlan.fromRow(row).loadChildren())); + return (await futures).toList(growable: false); + }); + } + + static Stream watchNutritionPlan(int id) { + return db.onChange([tableNutritionPlans, tableLogItems, tableMeals]).asyncMap((event) async { + final row = await db.get('SELECT * FROM $tableNutritionPlans WHERE id = ?', [id]); + return NutritionalPlan.fromRow(row).loadChildren(); + }); + } + + static Stream watchNutritionPlanLast() { + return db.onChange([tableNutritionPlans, tableLogItems, tableMeals]).asyncMap((event) async { + final row = + await db.get('SELECT * FROM $tableNutritionPlans ORDER BY creation_date DESC LIMIT 1'); + return NutritionalPlan.fromRow(row).loadChildren(); + }); + } +/* + static Stream> watchNutritionPlan(int id) { + return db + .watch('SELECT * FROM $tableNutritionPlans WHERE id = ?', parameters: [id]).map((results) { + return results.map(NutritionalPlan.fromRow).toList(growable: false); + }); + } + + static Stream> watchNutritionPlans() { + return db.watch('SELECT * FROM $tableNutritionPlans ORDER BY creation_date').map((results) { + return results.map(NutritionalPlan.fromRow).toList(growable: false); + }); + } + */ + + Future delete() async { + await db.execute('DELETE FROM $tableNutritionPlans WHERE id = ?', [id]); + } } diff --git a/lib/models/todo_list.dart b/lib/models/todo_list.dart deleted file mode 100644 index 4d965eb4c..000000000 --- a/lib/models/todo_list.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:powersync/sqlite3.dart' as sqlite; -import 'package:wger/powersync.dart'; - -import 'todo_item.dart'; - -/// TodoList represents a result row of a query on "lists". -/// -/// This class is immutable - methods on this class do not modify the instance -/// directly. Instead, watch or re-query the data to get the updated list. -class TodoList { - /// List id (UUID). - final String id; - - /// Descriptive name. - final String name; - - /// Number of completed todos in this list. - final int? completedCount; - - /// Number of pending todos in this list. - final int? pendingCount; - - TodoList({required this.id, required this.name, this.completedCount, this.pendingCount}); - - factory TodoList.fromRow(sqlite.Row row) { - return TodoList( - id: row['id'], - name: row['name'], - completedCount: row['completed_count'], - pendingCount: row['pending_count'], - ); - } - - /// Watch all lists. - static Stream> watchLists() { - // This query is automatically re-run when data in "lists" or "todos" is modified. - return db.watch('SELECT * FROM lists ORDER BY created_at, id').map((results) { - return results.map(TodoList.fromRow).toList(growable: false); - }); - } - - /// Watch all lists, with [completedCount] and [pendingCount] populated. - static Stream> watchListsWithStats() { - // This query is automatically re-run when data in "lists" or "todos" is modified. - return db.watch(''' - SELECT - *, - (SELECT count() FROM todos WHERE list_id = lists.id AND completed = TRUE) as completed_count, - (SELECT count() FROM todos WHERE list_id = lists.id AND completed = FALSE) as pending_count - FROM lists - ORDER BY created_at - ''').map((results) { - return results.map(TodoList.fromRow).toList(growable: false); - }); - } - - /// Create a new list - static Future create(String name) async { - final results = await db.execute( - ''' - INSERT INTO - lists(id, created_at, name, owner_id) - VALUES(uuid(), datetime(), ?, ?) - RETURNING * - ''', - [name, await getUserId()], - ); - return TodoList.fromRow(results.first); - } - - /// Watch items within this list. - Stream> watchItems() { - return db.watch('SELECT * FROM todos WHERE list_id = ? ORDER BY created_at DESC, id', - parameters: [id]).map((event) { - return event.map(TodoItem.fromRow).toList(growable: false); - }); - } - - /// Delete this list. - Future delete() async { - await db.execute('DELETE FROM lists WHERE id = ?', [id]); - } - - /// Find list item. - static Future find(id) async { - final results = await db.get('SELECT * FROM lists WHERE id = ?', [id]); - return TodoList.fromRow(results); - } - - /// Add a new todo item to this list. - Future add(String description) async { - final results = await db.execute(''' - INSERT INTO - todos(id, created_at, completed, list_id, description, created_by) - VALUES(uuid(), datetime(), FALSE, ?, ?, ?) - RETURNING * - ''', [id, description, await getUserId()]); - return TodoItem.fromRow(results.first); - } -} diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 6a5e11c6d..cda549ef0 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -88,59 +88,10 @@ class NutritionPlansProvider with ChangeNotifier { return null; } - /// Fetches and sets all plans sparsely, i.e. only with the data on the plan - /// object itself and no child attributes - Future fetchAndSetAllPlansSparse() async { - final data = await baseProvider.fetchPaginated( - baseProvider.makeUrl(_nutritionalPlansPath, query: {'limit': '1000'}), - ); - _plans = []; - for (final planData in data) { - final plan = NutritionalPlan.fromJson(planData); - _plans.add(plan); - _plans.sort((a, b) => b.creationDate.compareTo(a.creationDate)); - } - notifyListeners(); - } - - /// Fetches and sets all plans fully, i.e. with all corresponding child objects - Future fetchAndSetAllPlansFull() async { - final data = await baseProvider.fetchPaginated(baseProvider.makeUrl(_nutritionalPlansPath)); - await Future.wait(data.map((e) => fetchAndSetPlanFull(e['id'])).toList()); - } - - /// Fetches and sets the given nutritional plan - /// - /// This method only loads the data on the nutritional plan object itself, - /// no meals, etc. - Future fetchAndSetPlanSparse(int planId) async { - final url = baseProvider.makeUrl(_nutritionalPlansPath, id: planId); - final planData = await baseProvider.fetch(url); - final plan = NutritionalPlan.fromJson(planData); - _plans.add(plan); - _plans.sort((a, b) => b.creationDate.compareTo(a.creationDate)); - - notifyListeners(); - return plan; - } - /// Fetches a plan fully, i.e. with all corresponding child objects /// - - +/* Future fetchAndSetPlanFull(int planId) async { - NutritionalPlan plan; - try { - plan = findById(planId); - } on NoSuchEntryException { - // TODO: remove this useless call, because we will fetch all details below - plan = await fetchAndSetPlanSparse(planId); - } - - // Plan - final url = baseProvider.makeUrl(_nutritionalPlansInfoPath, id: planId); - final fullPlanData = await baseProvider.fetch(url); - // Meals final List meals = []; for (final mealData in fullPlanData['meals']) { @@ -173,7 +124,9 @@ class NutritionPlansProvider with ChangeNotifier { // ... and done notifyListeners(); return plan; + } + */ Future addPlan(NutritionalPlan planData) async { final data = await baseProvider.post( diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index 0d026ede5..a2b79217b 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -123,11 +123,10 @@ class _HomeTabsScreenState extends State with SingleTickerProvid } // Plans, weight and gallery - log.log(Level.FINER, 'Loading plans, weight, measurements and gallery'); + log.log(Level.FINER, 'Loading workouts, weight, measurements and gallery'); try { await Future.wait([ galleryProvider.fetchAndSetGallery(), - nutritionPlansProvider.fetchAndSetAllPlansSparse(), workoutPlansProvider.fetchAndSetAllPlansSparse(), weightProvider.fetchAndSetEntries(), measurementProvider.fetchAndSetAllCategoriesAndEntries(), @@ -137,18 +136,6 @@ class _HomeTabsScreenState extends State with SingleTickerProvid log.log(Level.FINER, e.toString()); } - // Current nutritional plan - log.log(Level.FINER, 'Loading current nutritional plan'); - try { - if (nutritionPlansProvider.currentPlan != null) { - final plan = nutritionPlansProvider.currentPlan!; - await nutritionPlansProvider.fetchAndSetPlanFull(plan.id!); - } - } catch (e) { - log.log(Level.FINER, 'Exception loading current nutritional plan'); - log.log(Level.FINER, e.toString()); - } - // Current workout plan log.log(Level.FINER, 'Loading current workout plan'); if (workoutPlansProvider.activePlan != null) { diff --git a/lib/screens/nutritional_plan_screen.dart b/lib/screens/nutritional_plan_screen.dart index bb13be6cd..5459276f8 100644 --- a/lib/screens/nutritional_plan_screen.dart +++ b/lib/screens/nutritional_plan_screen.dart @@ -36,10 +36,6 @@ class NutritionalPlanScreen extends StatelessWidget { const NutritionalPlanScreen(); static const routeName = '/nutritional-plan-detail'; - Future _loadFullPlan(BuildContext context, int planId) { - return Provider.of(context, listen: false).fetchAndSetPlanFull(planId); - } - @override Widget build(BuildContext context) { const appBarForeground = Colors.white; @@ -158,7 +154,7 @@ class NutritionalPlanScreen extends StatelessWidget { ), ), FutureBuilder( - future: _loadFullPlan(context, nutritionalPlan.id!), + future: NutritionalPlan.read(nutritionalPlan.id!), builder: (context, AsyncSnapshot snapshot) => snapshot.connectionState == ConnectionState.waiting ? SliverList( diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index 65ee1c39c..b49ee5100 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import 'dart:async'; + import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -56,13 +58,29 @@ class DashboardNutritionWidget extends StatefulWidget { class _DashboardNutritionWidgetState extends State { NutritionalPlan? _plan; + StreamSubscription? _subscription; + bool _hasContent = false; @override void initState() { super.initState(); - _plan = Provider.of(context, listen: false).currentPlan; - _hasContent = _plan != null; + final stream = NutritionalPlan.watchNutritionPlanLast(); + _subscription = stream.listen((plan) { + if (!context.mounted) { + return; + } + setState(() { + _plan = plan; + _hasContent = _plan != null; + }); + }); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); } @override diff --git a/lib/widgets/nutrition/nutritional_plans_list.dart b/lib/widgets/nutrition/nutritional_plans_list.dart index d1a6a48e0..15beca293 100644 --- a/lib/widgets/nutrition/nutritional_plans_list.dart +++ b/lib/widgets/nutrition/nutritional_plans_list.dart @@ -30,87 +30,83 @@ class NutritionalPlansList extends StatelessWidget { @override Widget build(BuildContext context) { - return RefreshIndicator( - onRefresh: () => _nutritionProvider.fetchAndSetAllPlansSparse(), - child: _nutritionProvider.items.isEmpty - ? const TextPrompt() - : ListView.builder( - padding: const EdgeInsets.all(10.0), - itemCount: _nutritionProvider.items.length, - itemBuilder: (context, index) { - final currentPlan = _nutritionProvider.items[index]; - return Card( - child: ListTile( - onTap: () { - Navigator.of(context).pushNamed( - NutritionalPlanScreen.routeName, - arguments: currentPlan, - ); - }, - title: Text(currentPlan.getLabel(context)), - subtitle: Text( - DateFormat.yMd( - Localizations.localeOf(context).languageCode, - ).format(currentPlan.creationDate), - ), - trailing: Row(mainAxisSize: MainAxisSize.min, children: [ - const VerticalDivider(), - IconButton( - icon: const Icon(Icons.delete), - tooltip: AppLocalizations.of(context).delete, - onPressed: () async { - // Delete the plan from DB - await showDialog( - context: context, - builder: (BuildContext contextDialog) { - return AlertDialog( - content: Text( - AppLocalizations.of(context) - .confirmDelete(currentPlan.description), + return _nutritionProvider.items.isEmpty + ? const TextPrompt() + : ListView.builder( + padding: const EdgeInsets.all(10.0), + itemCount: _nutritionProvider.items.length, + itemBuilder: (context, index) { + final currentPlan = _nutritionProvider.items[index]; + return Card( + child: ListTile( + onTap: () { + Navigator.of(context).pushNamed( + NutritionalPlanScreen.routeName, + arguments: currentPlan, + ); + }, + title: Text(currentPlan.getLabel(context)), + subtitle: Text( + DateFormat.yMd( + Localizations.localeOf(context).languageCode, + ).format(currentPlan.creationDate), + ), + trailing: Row(mainAxisSize: MainAxisSize.min, children: [ + const VerticalDivider(), + IconButton( + icon: const Icon(Icons.delete), + tooltip: AppLocalizations.of(context).delete, + onPressed: () async { + // Delete the plan from DB + await showDialog( + context: context, + builder: (BuildContext contextDialog) { + return AlertDialog( + content: Text( + AppLocalizations.of(context).confirmDelete(currentPlan.description), + ), + actions: [ + TextButton( + child: Text( + MaterialLocalizations.of(context).cancelButtonLabel, + ), + onPressed: () => Navigator.of(contextDialog).pop(), ), - actions: [ - TextButton( - child: Text( - MaterialLocalizations.of(context).cancelButtonLabel, + TextButton( + child: Text( + AppLocalizations.of(context).delete, + style: TextStyle( + color: Theme.of(context).colorScheme.error, ), - onPressed: () => Navigator.of(contextDialog).pop(), ), - TextButton( - child: Text( - AppLocalizations.of(context).delete, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ), - onPressed: () { - // Confirmed, delete the plan - _nutritionProvider.deletePlan(currentPlan.id!); + onPressed: () { + // Confirmed, delete the plan + _nutritionProvider.deletePlan(currentPlan.id!); - // Close the popup - Navigator.of(contextDialog).pop(); + // Close the popup + Navigator.of(contextDialog).pop(); - // and inform the user - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - AppLocalizations.of(context).successfullyDeleted, - textAlign: TextAlign.center, - ), + // and inform the user + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).successfullyDeleted, + textAlign: TextAlign.center, ), - ); - }, - ), - ], - ); - }, - ); - }, - ), - ]), - ), - ); - }, - ), - ); + ), + ); + }, + ), + ], + ); + }, + ); + }, + ), + ]), + ), + ); + }, + ); } } From ea60e18f18f2ccb20bda0b9466cb704c9361960b Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Wed, 18 Sep 2024 10:33:06 +0300 Subject: [PATCH 12/17] WIP fixes --- lib/models/nutrition/log.dart | 44 ++++++++++++++++------------------ lib/models/nutrition/meal.dart | 6 +++-- lib/models/schema.dart | 15 ++++++++++++ lib/providers/nutrition.dart | 36 ++-------------------------- 4 files changed, 42 insertions(+), 59 deletions(-) diff --git a/lib/models/nutrition/log.dart b/lib/models/nutrition/log.dart index 5b56fe8b1..98a7952d9 100644 --- a/lib/models/nutrition/log.dart +++ b/lib/models/nutrition/log.dart @@ -25,7 +25,7 @@ import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; import 'package:wger/models/schema.dart'; import 'package:wger/powersync.dart'; -import 'package:powersync/sqlite3.dart' as sqlite; +import 'package:wger/providers/nutrition.dart'; part 'log.g.dart'; @@ -82,11 +82,11 @@ class Log { factory Log.fromRow(sqlite.Row row) { return Log( id: int.parse(row['id']), - mealId: int.parse(row['meal_id']), - ingredientId: int.parse(row['ingredient_id']), - weightUnitId: int.parse(row['weight_unit_id']), + mealId: row['meal_id'], + ingredientId: row['ingredient_id'], + weightUnitId: row['weight_unit_id'], amount: row['amount'], - planId: int.parse(row['plan_id']), + planId: row['plan_id'], datetime: DateTime.parse(row['datetime']), comment: row['comment'], ); @@ -109,7 +109,22 @@ class Log { static Future> readByPlanId(int planId) async { final results = await db.getAll('SELECT * FROM $tableLogItems WHERE plan_id = ?', [planId]); - return results.map((r) => Log.fromRow(r)).toList(); + return results.map((r) { + final log = Log.fromRow(r); + // TODO: + // need to find a way to set ingredients. since we don't use powersync for it, we need to fetch + // but this needs a context, therofere this needs a context, and all callers do, so we should probably + // move all that stuff into the nutritionprovider, so we keep context out of the models + // however, still unsolved: + // mealItem stuff then? + // nutrition image + // nutrition_ingredientcategory + // nutrition_ingredientweightunit + // nutrition_weightunit; + // nutrition_mealitem + log.ingredient = Provider.of(context, listen: false).fetchIngredient(id), + return log; + }).toList(); } /* @@ -121,22 +136,5 @@ class Log { await db.execute('UPDATE $logItemsTable SET photo_id = ? WHERE id = ?', [photoId, id]); } } - - static Stream> watchLists() { - // This query is automatically re-run when data in "lists" or "todos" is modified. - return db.watch('SELECT * FROM lists ORDER BY created_at, id').map((results) { - return results.map(TodoList.fromRow).toList(growable: false); - }); - } - - static Future create(String name) async { - final results = await db.execute(''' - INSERT INTO - lists(id, created_at, name, owner_id) - VALUES(uuid(), datetime(), ?, ?) - RETURNING * - ''', [name, await getUserId()]); - return TodoList.fromRow(results.first); - } */ } diff --git a/lib/models/nutrition/meal.dart b/lib/models/nutrition/meal.dart index ea7080f8c..76128b09b 100644 --- a/lib/models/nutrition/meal.dart +++ b/lib/models/nutrition/meal.dart @@ -18,13 +18,13 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/helpers/misc.dart'; import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; -import 'package:powersync/sqlite3.dart' as sqlite; import 'package:wger/models/schema.dart'; import 'package:wger/powersync.dart'; @@ -93,7 +93,7 @@ class Meal { factory Meal.fromRow(sqlite.Row row) { return Meal( id: int.parse(row['id']), - plan: int.parse(row['plan']), + plan: row['plan'], time: stringToTime(row['time']), name: row['name'], ); @@ -125,7 +125,9 @@ class Meal { } static Future> readByPlanId(int planId) async { + print('Meal.readByPlanId: SELECT * FROM $tableMeals WHERE plan_id = $planId'); final results = await db.getAll('SELECT * FROM $tableMeals WHERE plan_id = ?', [planId]); + print(results.rows.length); return results.map((r) => Meal.fromRow(r)).toList(); } } diff --git a/lib/models/schema.dart b/lib/models/schema.dart index b964c4c61..0b5b72c76 100644 --- a/lib/models/schema.dart +++ b/lib/models/schema.dart @@ -1,5 +1,20 @@ import 'package:powersync/powersync.dart'; +/* nutrition tables in postgres: +| public | nutrition_image | table> +| public | nutrition_ingredient | table> * # millions of ingredients +| public | nutrition_ingredientcategory | table> +| public | nutrition_ingredientweightunit | table> +| public | nutrition_logitem | table> * OK +| public | nutrition_meal | table> * OK +| public | nutrition_mealitem | table> * +| public | nutrition_nutritionplan | table> * OK +| public | nutrition_weightunit | table> + +assumptions: nutrition_ingredientcategory, nutrition_weightunit, nutrition_ingredientweightunit globals? +*/ + +// User,NutritionPlan,Meal,LogItem,MealItem,Ingredient const tableMuscles = 'exercises_muscle'; const tableLogItems = 'nutrition_logitem'; const tableNutritionPlans = 'nutrition_nutritionplan'; diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index cda549ef0..0bda89434 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -34,6 +34,7 @@ import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/providers/base_provider.dart'; class NutritionPlansProvider with ChangeNotifier { + // TODO: should be able to delete many of these paths and their corresponding code static const _nutritionalPlansPath = 'nutritionplan'; static const _nutritionalPlansInfoPath = 'nutritionplaninfo'; static const _mealPath = 'meal'; @@ -88,44 +89,11 @@ class NutritionPlansProvider with ChangeNotifier { return null; } - /// Fetches a plan fully, i.e. with all corresponding child objects - /// /* - Future fetchAndSetPlanFull(int planId) async { - // Meals - final List meals = []; - for (final mealData in fullPlanData['meals']) { - final List mealItems = []; - final meal = Meal.fromJson(mealData); - - // TODO: we should add these ingredients to the ingredient cache - for (final mealItemData in mealData['meal_items']) { - final mealItem = MealItem.fromJson(mealItemData); - - final ingredient = Ingredient.fromJson(mealItemData['ingredient_obj']); - if (mealItemData['image'] != null) { - final image = IngredientImage.fromJson(mealItemData['image']); +TODO implement: ingredient.image = image; - } mealItem.ingredient = ingredient; - mealItems.add(mealItem); - } - meal.mealItems = mealItems; - meals.add(meal); - } - plan.meals = meals; - - // Logs - await fetchAndSetLogs(plan); - for (final meal in meals) { - meal.diaryEntries = plan.diaryEntries.where((e) => e.mealId == meal.id).toList(); - } - // ... and done - notifyListeners(); - return plan; - - } */ Future addPlan(NutritionalPlan planData) async { From 51ec089d76b30e34a5a35f13ecd5a33a2d7704dc Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Wed, 16 Oct 2024 17:55:15 +0300 Subject: [PATCH 13/17] progress reading data, using ivm tables --- lib/models/nutrition/log.dart | 22 +- lib/models/nutrition/meal.dart | 16 +- lib/models/nutrition/meal_item.dart | 18 ++ lib/models/nutrition/nutritional_plan.dart | 6 +- lib/models/schema.dart | 6 +- lib/providers/nutrition.dart | 61 ++++ lib/screens/dashboard.dart | 1 - lib/screens/nutritional_diary_screen.dart | 39 ++- lib/screens/nutritional_plan_screen.dart | 284 ++++++++++-------- lib/screens/nutritional_plans_screen.dart | 2 +- lib/widgets/dashboard/widgets.dart | 5 +- lib/widgets/nutrition/forms.dart | 2 +- .../nutrition/nutritional_diary_table.dart | 2 +- .../nutrition/nutritional_plans_list.dart | 53 +++- test/nutrition/nutrition_provider_test.dart | 2 +- 15 files changed, 344 insertions(+), 175 deletions(-) diff --git a/lib/models/nutrition/log.dart b/lib/models/nutrition/log.dart index 98a7952d9..0d3a61a66 100644 --- a/lib/models/nutrition/log.dart +++ b/lib/models/nutrition/log.dart @@ -107,24 +107,14 @@ class Log { return ingredient.nutritionalValues / (100 / weight); } + static Future> readByMealId(int mealId) async { + final results = await db.getAll('SELECT * FROM $tableLogItems WHERE meal_id = ?', [mealId]); + return results.map((r) => Log.fromRow(r)).toList(); + } + static Future> readByPlanId(int planId) async { final results = await db.getAll('SELECT * FROM $tableLogItems WHERE plan_id = ?', [planId]); - return results.map((r) { - final log = Log.fromRow(r); - // TODO: - // need to find a way to set ingredients. since we don't use powersync for it, we need to fetch - // but this needs a context, therofere this needs a context, and all callers do, so we should probably - // move all that stuff into the nutritionprovider, so we keep context out of the models - // however, still unsolved: - // mealItem stuff then? - // nutrition image - // nutrition_ingredientcategory - // nutrition_ingredientweightunit - // nutrition_weightunit; - // nutrition_mealitem - log.ingredient = Provider.of(context, listen: false).fetchIngredient(id), - return log; - }).toList(); + return results.map((r) => Log.fromRow(r)).toList(); } /* diff --git a/lib/models/nutrition/meal.dart b/lib/models/nutrition/meal.dart index 76128b09b..3057220b7 100644 --- a/lib/models/nutrition/meal.dart +++ b/lib/models/nutrition/meal.dart @@ -93,7 +93,7 @@ class Meal { factory Meal.fromRow(sqlite.Row row) { return Meal( id: int.parse(row['id']), - plan: row['plan'], + plan: row['plan_id'], time: stringToTime(row['time']), name: row['name'], ); @@ -103,7 +103,7 @@ class Meal { Meal copyWith({ int? id, - int? planId, + int? plan, TimeOfDay? time, String? name, List? mealItems, @@ -111,7 +111,7 @@ class Meal { }) { return Meal( id: id ?? this.id, - plan: planId ?? this.planId, + plan: plan ?? planId, time: time ?? this.time, name: name ?? this.name, mealItems: mealItems ?? this.mealItems, @@ -119,6 +119,14 @@ class Meal { ); } + Future loadChildren() async { + print('loadChildren called. plan is $planId'); + return copyWith( + mealItems: await MealItem.readByMealId(id!), + diaryEntries: await Log.readByMealId(id!), + ); + } + static Future read(int id) async { final results = await db.get('SELECT * FROM $tableMeals WHERE id = ?', [id]); return Meal.fromRow(results); @@ -128,6 +136,6 @@ class Meal { print('Meal.readByPlanId: SELECT * FROM $tableMeals WHERE plan_id = $planId'); final results = await db.getAll('SELECT * FROM $tableMeals WHERE plan_id = ?', [planId]); print(results.rows.length); - return results.map((r) => Meal.fromRow(r)).toList(); + return Future.wait(results.map((r) => Meal.fromRow(r).loadChildren())); } } diff --git a/lib/models/nutrition/meal_item.dart b/lib/models/nutrition/meal_item.dart index 410e43064..811f86e99 100644 --- a/lib/models/nutrition/meal_item.dart +++ b/lib/models/nutrition/meal_item.dart @@ -17,10 +17,14 @@ */ import 'package:json_annotation/json_annotation.dart'; +import 'package:powersync/sqlite3.dart' as sqlite; + import 'package:wger/helpers/json.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/ingredient_weight_unit.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; +import 'package:wger/models/schema.dart'; +import 'package:wger/powersync.dart'; part 'meal_item.g.dart'; @@ -71,6 +75,15 @@ class MealItem { Map toJson() => _$MealItemToJson(this); + factory MealItem.fromRow(sqlite.Row row) { + return MealItem( + amount: row['amount'], + weightUnitId: row['weight_unit_id'], + mealId: row['meal_id'], + ingredientId: row['ingredient_id'], + ); + } + /// Calculations /// TODO why does this not consider weightUnitObj ? should we do the same as Log.nutritionalValues here? NutritionalValues get nutritionalValues { @@ -112,4 +125,9 @@ class MealItem { m.weightUnitObj = weightUnitObj ?? this.weightUnitObj; return m; } + + static Future> readByMealId(int mealId) async { + final results = await db.getAll('SELECT * FROM $tableMealItems WHERE meal_id = ?', [mealId]); + return results.map((r) => MealItem.fromRow(r)).toList(); + } } diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index fe635aa19..213fdfd4b 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -327,10 +327,10 @@ class NutritionalPlan { }); } - static Stream watchNutritionPlan(int id) { + static Stream watchNutritionPlan(int id) { return db.onChange([tableNutritionPlans, tableLogItems, tableMeals]).asyncMap((event) async { - final row = await db.get('SELECT * FROM $tableNutritionPlans WHERE id = ?', [id]); - return NutritionalPlan.fromRow(row).loadChildren(); + final row = await db.getOptional('SELECT * FROM $tableNutritionPlans WHERE id = ?', [id]); + return row == null ? null : NutritionalPlan.fromRow(row).loadChildren(); }); } diff --git a/lib/models/schema.dart b/lib/models/schema.dart index 0b5b72c76..551e1a019 100644 --- a/lib/models/schema.dart +++ b/lib/models/schema.dart @@ -16,10 +16,10 @@ assumptions: nutrition_ingredientcategory, nutrition_weightunit, nutrition_ingre // User,NutritionPlan,Meal,LogItem,MealItem,Ingredient const tableMuscles = 'exercises_muscle'; -const tableLogItems = 'nutrition_logitem'; +const tableLogItems = 'ivm_nutrition_logitem'; const tableNutritionPlans = 'nutrition_nutritionplan'; -const tableMeals = 'nutrition_meal'; -const tableMealItems = 'nutrition_mealitem'; +const tableMeals = 'ivm_nutrition_meal'; +const tableMealItems = 'ivm_nutrition_mealitem'; Schema schema = const Schema([ Table( diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 9eec56a4b..4e8261190 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import 'dart:async'; import 'dart:convert'; import 'dart:developer'; @@ -91,6 +92,66 @@ class NutritionPlansProvider with ChangeNotifier { return null; } + Future _enrichPlan(NutritionalPlan plan) async { + // TODO: set up ingredient images + + final List diaryEntries = []; + for (final diaryEntry in plan.diaryEntries) { + diaryEntry.ingredient = await fetchIngredient(diaryEntry.ingredientId); + diaryEntries.add(diaryEntry); + } + + final List meals = []; + for (final meal in plan.meals) { + final List mealItems = []; + for (final mealItem in meal.mealItems) { + mealItem.ingredient = await fetchIngredient(mealItem.ingredientId); + mealItems.add(mealItem); + } + meal.mealItems = mealItems; + meal.diaryEntries = diaryEntries.where((d) => d.mealId == meal.id).toList(); + meals.add(meal); + } + + plan.meals = meals; + plan.diaryEntries = diaryEntries; + + return plan; + } + + Stream watchNutritionPlan(int id) { + return NutritionalPlan.watchNutritionPlan(id).transform( + StreamTransformer.fromHandlers( + handleData: (plan, sink) async { + if (plan == null) { + sink.add(plan); + return; + } + sink.add(await _enrichPlan(plan)); + }, + ), + ); + } + + Stream watchNutritionPlanLast() { + return NutritionalPlan.watchNutritionPlanLast().transform( + StreamTransformer.fromHandlers( + handleData: (plan, sink) async { + sink.add(await _enrichPlan(plan)); + }, + ), + ); + } + + Stream> watchNutritionPlans() { + return NutritionalPlan.watchNutritionPlans().transform( + StreamTransformer.fromHandlers( + handleData: (plans, sink) async { + sink.add(await Future.wait(plans.map((plan) => _enrichPlan(plan)))); + }, + ), + ); + } /* TODO implement: ingredient.image = image; diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 43a0fbde6..35ab9f52e 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -42,7 +42,6 @@ class _DashboardScreenState extends State { padding: EdgeInsets.all(10), child: Column( children: [ - DashboardMuscleWidget(), DashboardWorkoutWidget(), DashboardNutritionWidget(), DashboardWeightWidget(), diff --git a/lib/screens/nutritional_diary_screen.dart b/lib/screens/nutritional_diary_screen.dart index 4e60f44f0..1ea1c15c4 100644 --- a/lib/screens/nutritional_diary_screen.dart +++ b/lib/screens/nutritional_diary_screen.dart @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -26,7 +28,7 @@ import 'package:wger/widgets/nutrition/nutritional_diary_detail.dart'; /// Arguments passed to the form screen class NutritionalDiaryArguments { /// Nutritional plan - final NutritionalPlan plan; + final int plan; /// Date to show data for final DateTime date; @@ -34,23 +36,50 @@ class NutritionalDiaryArguments { const NutritionalDiaryArguments(this.plan, this.date); } -class NutritionalDiaryScreen extends StatelessWidget { +class NutritionalDiaryScreen extends StatefulWidget { const NutritionalDiaryScreen(); static const routeName = '/nutritional-diary'; @override - Widget build(BuildContext context) { + State createState() => _NutritionalDiaryScreenState(); +} + +class _NutritionalDiaryScreenState extends State { + NutritionalPlan? _plan; + late DateTime date; + StreamSubscription? _subscription; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); final args = ModalRoute.of(context)!.settings.arguments as NutritionalDiaryArguments; + date = args.date; + + final stream = + Provider.of(context, listen: false).watchNutritionPlan(args.plan); + _subscription = stream.listen((plan) { + if (!context.mounted) { + return; + } + setState(() { + _plan = plan; + }); + }); + } + @override + Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(DateFormat.yMd(Localizations.localeOf(context).languageCode).format(args.date)), + title: Text(DateFormat.yMd(Localizations.localeOf(context).languageCode).format(date)), ), body: Consumer( builder: (context, nutritionProvider, child) => SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), - child: NutritionalDiaryDetailWidget(args.plan, args.date), + child: _plan == null + ? const Text('plan not found') + : NutritionalDiaryDetailWidget(_plan!, date), ), ), ), diff --git a/lib/screens/nutritional_plan_screen.dart b/lib/screens/nutritional_plan_screen.dart index 0849d2cd9..6201433d8 100644 --- a/lib/screens/nutritional_plan_screen.dart +++ b/lib/screens/nutritional_plan_screen.dart @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg_icons/flutter_svg_icons.dart'; @@ -32,153 +34,189 @@ enum NutritionalPlanOptions { delete, } -class NutritionalPlanScreen extends StatelessWidget { +class NutritionalPlanScreen extends StatefulWidget { const NutritionalPlanScreen(); static const routeName = '/nutritional-plan-detail'; + @override + _NutritionalPlanScreenState createState() => _NutritionalPlanScreenState(); +} + +class _NutritionalPlanScreenState extends State { + NutritionalPlan? _plan; + StreamSubscription? _subscription; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final id = ModalRoute.of(context)!.settings.arguments as int; + //final id = 111; + + final stream = + Provider.of(context, listen: false).watchNutritionPlan(id); + _subscription = stream.listen((plan) { + if (!context.mounted) { + return; + } + setState(() { + _plan = plan; + }); + }); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { const appBarForeground = Colors.white; - final nutritionalPlan = ModalRoute.of(context)!.settings.arguments as NutritionalPlan; return Scaffold( //appBar: getAppBar(nutritionalPlan), - floatingActionButton: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton( - heroTag: null, - tooltip: AppLocalizations.of(context).logIngredient, - onPressed: () { - Navigator.pushNamed( - context, - FormScreen.routeName, - arguments: FormScreenArguments( - AppLocalizations.of(context).logIngredient, - IngredientLogForm(nutritionalPlan), - hasListView: true, - ), - ); - }, - child: const SvgIcon( - icon: SvgIconData('assets/icons/ingredient-diary.svg'), - color: Colors.white, - ), - ), - const SizedBox(width: 8), - FloatingActionButton( - heroTag: null, - tooltip: AppLocalizations.of(context).logMeal, - onPressed: () { - Navigator.of(context).pushNamed( - LogMealsScreen.routeName, - arguments: nutritionalPlan, - ); - }, - child: const SvgIcon( - icon: SvgIconData('assets/icons/meal-diary.svg'), - color: Colors.white, - ), - ), - ], - ), - body: CustomScrollView( - slivers: [ - SliverAppBar( - foregroundColor: appBarForeground, - pinned: true, - iconTheme: const IconThemeData(color: appBarForeground), - actions: [ - if (!nutritionalPlan.onlyLogging) - IconButton( - icon: const SvgIcon( - icon: SvgIconData('assets/icons/meal-add.svg'), - ), + floatingActionButton: _plan == null + ? const Offstage() + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: null, + tooltip: AppLocalizations.of(context).logIngredient, onPressed: () { Navigator.pushNamed( context, FormScreen.routeName, arguments: FormScreenArguments( - AppLocalizations.of(context).addMeal, - MealForm(nutritionalPlan.id!), + AppLocalizations.of(context).logIngredient, + IngredientLogForm(_plan!), + hasListView: true, ), ); }, + child: const SvgIcon( + icon: SvgIconData('assets/icons/ingredient-diary.svg'), + color: Colors.white, + ), ), - PopupMenuButton( - icon: const Icon(Icons.more_vert, color: appBarForeground), - onSelected: (value) { - switch (value) { - case NutritionalPlanOptions.edit: - Navigator.pushNamed( - context, - FormScreen.routeName, - arguments: FormScreenArguments( - AppLocalizations.of(context).edit, - PlanForm(nutritionalPlan), - hasListView: true, + const SizedBox(width: 8), + FloatingActionButton( + heroTag: null, + tooltip: AppLocalizations.of(context).logMeal, + onPressed: () { + Navigator.of(context).pushNamed( + LogMealsScreen.routeName, + arguments: _plan, + ); + }, + child: const SvgIcon( + icon: SvgIconData('assets/icons/meal-diary.svg'), + color: Colors.white, + ), + ), + ], + ), + body: _plan == null + ? const Text('plan not found') + : CustomScrollView( + slivers: [ + SliverAppBar( + foregroundColor: appBarForeground, + pinned: true, + iconTheme: const IconThemeData(color: appBarForeground), + actions: [ + if (!_plan!.onlyLogging) + IconButton( + icon: const SvgIcon( + icon: SvgIconData('assets/icons/meal-add.svg'), ), - ); - break; - case NutritionalPlanOptions.delete: - Provider.of(context, listen: false) - .deletePlan(nutritionalPlan.id!); - Navigator.of(context).pop(); - break; - } - }, - itemBuilder: (BuildContext context) { - return [ - PopupMenuItem( - value: NutritionalPlanOptions.edit, - child: ListTile( - leading: const Icon(Icons.edit), - title: Text(AppLocalizations.of(context).edit), + onPressed: () { + Navigator.pushNamed( + context, + FormScreen.routeName, + arguments: FormScreenArguments( + AppLocalizations.of(context).addMeal, + MealForm(_plan!.id!), + ), + ); + }, ), + PopupMenuButton( + icon: const Icon(Icons.more_vert, color: appBarForeground), + onSelected: (value) { + switch (value) { + case NutritionalPlanOptions.edit: + Navigator.pushNamed( + context, + FormScreen.routeName, + arguments: FormScreenArguments( + AppLocalizations.of(context).edit, + PlanForm(_plan), + hasListView: true, + ), + ); + break; + case NutritionalPlanOptions.delete: + Provider.of(context, listen: false) + .deletePlan(_plan!.id!); + Navigator.of(context).pop(); + break; + } + }, + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + value: NutritionalPlanOptions.edit, + child: ListTile( + leading: const Icon(Icons.edit), + title: Text(AppLocalizations.of(context).edit), + ), + ), + const PopupMenuDivider(), + PopupMenuItem( + value: NutritionalPlanOptions.delete, + child: ListTile( + leading: const Icon(Icons.delete), + title: Text(AppLocalizations.of(context).delete), + ), + ), + ]; + }, ), - const PopupMenuDivider(), - PopupMenuItem( - value: NutritionalPlanOptions.delete, - child: ListTile( - leading: const Icon(Icons.delete), - title: Text(AppLocalizations.of(context).delete), - ), + ], + flexibleSpace: FlexibleSpaceBar( + titlePadding: const EdgeInsets.fromLTRB(56, 0, 56, 16), + title: Text( + _plan!.getLabel(context), + style: + Theme.of(context).textTheme.titleLarge?.copyWith(color: appBarForeground), ), - ]; - }, - ), - ], - flexibleSpace: FlexibleSpaceBar( - titlePadding: const EdgeInsets.fromLTRB(56, 0, 56, 16), - title: Text( - nutritionalPlan.getLabel(context), - style: Theme.of(context).textTheme.titleLarge?.copyWith(color: appBarForeground), - ), - ), - ), - FutureBuilder( - future: NutritionalPlan.read(nutritionalPlan.id!), - builder: (context, AsyncSnapshot snapshot) => - snapshot.connectionState == ConnectionState.waiting - ? SliverList( - delegate: SliverChildListDelegate( - [ - const SizedBox( - height: 200, - child: Center( - child: CircularProgressIndicator(), + ), + ), + FutureBuilder( + future: NutritionalPlan.read(_plan!.id!), + builder: (context, AsyncSnapshot snapshot) => + snapshot.connectionState == ConnectionState.waiting + ? SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox( + height: 200, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ], ), + ) + : Consumer( + builder: (context, value, child) => + NutritionalPlanDetailWidget(_plan!), ), - ], - ), - ) - : Consumer( - builder: (context, value, child) => - NutritionalPlanDetailWidget(nutritionalPlan), - ), - ), - ], - ), + ), + ], + ), ); } } diff --git a/lib/screens/nutritional_plans_screen.dart b/lib/screens/nutritional_plans_screen.dart index 43ce024c4..d6c9e706f 100644 --- a/lib/screens/nutritional_plans_screen.dart +++ b/lib/screens/nutritional_plans_screen.dart @@ -48,7 +48,7 @@ class NutritionalPlansScreen extends StatelessWidget { }, ), body: Consumer( - builder: (context, nutritionProvider, child) => NutritionalPlansList(nutritionProvider), + builder: (context, nutritionProvider, child) => NutritionalPlansList(), ), ); } diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index e93d8ef5b..e0709bd19 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -65,7 +65,8 @@ class _DashboardNutritionWidgetState extends State { @override void initState() { super.initState(); - final stream = NutritionalPlan.watchNutritionPlanLast(); + final stream = + Provider.of(context, listen: false).watchNutritionPlanLast(); _subscription = stream.listen((plan) { if (!context.mounted) { return; @@ -127,7 +128,7 @@ class _DashboardNutritionWidgetState extends State { onPressed: () { Navigator.of(context).pushNamed( NutritionalPlanScreen.routeName, - arguments: _plan, + arguments: _plan!.id, ); }, ), diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index ec336f376..785e9bacb 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -672,7 +672,7 @@ class _PlanFormState extends State { if (context.mounted) { Navigator.of(context).pushReplacementNamed( NutritionalPlanScreen.routeName, - arguments: widget._plan, + arguments: widget._plan.id, ); } } diff --git a/lib/widgets/nutrition/nutritional_diary_table.dart b/lib/widgets/nutrition/nutritional_diary_table.dart index ec7dddc87..f69bb814d 100644 --- a/lib/widgets/nutrition/nutritional_diary_table.dart +++ b/lib/widgets/nutrition/nutritional_diary_table.dart @@ -117,7 +117,7 @@ class NutritionalDiaryTable extends StatelessWidget { return GestureDetector( onTap: () => Navigator.of(context).pushNamed( NutritionalDiaryScreen.routeName, - arguments: NutritionalDiaryArguments(plan, date), + arguments: NutritionalDiaryArguments(plan.id!, date), ), child: element, ); diff --git a/lib/widgets/nutrition/nutritional_plans_list.dart b/lib/widgets/nutrition/nutritional_plans_list.dart index 15beca293..16449e288 100644 --- a/lib/widgets/nutrition/nutritional_plans_list.dart +++ b/lib/widgets/nutrition/nutritional_plans_list.dart @@ -16,33 +16,64 @@ * along with this program. If not, see . */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/screens/nutritional_plan_screen.dart'; import 'package:wger/widgets/core/text_prompt.dart'; -class NutritionalPlansList extends StatelessWidget { - final NutritionPlansProvider _nutritionProvider; +class NutritionalPlansList extends StatefulWidget { + @override + _NutritionalPlansListState createState() => _NutritionalPlansListState(); +} + +class _NutritionalPlansListState extends State { + List _plans = []; + StreamSubscription? _subscription; + + @override + void initState() { + super.initState(); + final stream = + Provider.of(context, listen: false).watchNutritionPlans(); + _subscription = stream.listen((plans) { + if (!context.mounted) { + return; + } + setState(() { + _plans = plans; + }); + }); + } - const NutritionalPlansList(this._nutritionProvider); + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } @override Widget build(BuildContext context) { - return _nutritionProvider.items.isEmpty + final provider = Provider.of(context, listen: false); + + return _plans.isEmpty ? const TextPrompt() : ListView.builder( padding: const EdgeInsets.all(10.0), - itemCount: _nutritionProvider.items.length, + itemCount: _plans.length, itemBuilder: (context, index) { - final currentPlan = _nutritionProvider.items[index]; + final currentPlan = _plans[index]; return Card( child: ListTile( onTap: () { Navigator.of(context).pushNamed( NutritionalPlanScreen.routeName, - arguments: currentPlan, + arguments: currentPlan.id, ); }, title: Text(currentPlan.getLabel(context)), @@ -57,7 +88,6 @@ class NutritionalPlansList extends StatelessWidget { icon: const Icon(Icons.delete), tooltip: AppLocalizations.of(context).delete, onPressed: () async { - // Delete the plan from DB await showDialog( context: context, builder: (BuildContext contextDialog) { @@ -80,13 +110,8 @@ class NutritionalPlansList extends StatelessWidget { ), ), onPressed: () { - // Confirmed, delete the plan - _nutritionProvider.deletePlan(currentPlan.id!); - - // Close the popup + provider.deletePlan(currentPlan.id!); Navigator.of(contextDialog).pop(); - - // and inform the user ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( diff --git a/test/nutrition/nutrition_provider_test.dart b/test/nutrition/nutrition_provider_test.dart index 2b8fb842a..d7922df24 100644 --- a/test/nutrition/nutrition_provider_test.dart +++ b/test/nutrition/nutrition_provider_test.dart @@ -103,7 +103,7 @@ void main() { group('fetchAndSetPlanFull', () { test('should correctly load a full nutritional plan', () async { // arrange - await nutritionProvider.fetchAndSetPlanFull(1); + // await nutritionProvider.fetchAndSetPlanFull(1); // assert expect(nutritionProvider.items.isEmpty, false); From d041b06bb2feb54a6fcca55ea6d30c008744acde Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Wed, 16 Oct 2024 22:06:03 +0300 Subject: [PATCH 14/17] flutter says we need to upgrade the kotlin gradle plugin --- android/settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/settings.gradle b/android/settings.gradle index ef03c12b4..1c38bbdf1 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.1" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "2.0.21" apply false } include ":app" From 5019ba398d678322e7c015adf82e8b2fcd81c085 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Thu, 17 Oct 2024 11:28:54 +0300 Subject: [PATCH 15/17] fix version conflict --- pubspec.lock | 18 +++++++++--------- pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 94914a2ef..f7c40298b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1088,18 +1088,18 @@ packages: dependency: "direct main" description: name: powersync - sha256: c6975007493617fdfc5945c3fab24ea2e6999ae300dd4d19d739713a4f2bcd96 + sha256: "7f1d2f38a936d3afd82447c4aee3b103929c6987beeae8353ccb135fe7490534" url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.8.6" powersync_flutter_libs: dependency: transitive description: name: powersync_flutter_libs - sha256: "449063aa4956c6be215ea7dfb9cc61255188e82cf7bc3f75621796fcc6615b70" + sha256: "9cddbbc91a5887eb54297fc8f189aff76ca5f70988eaf702cf46d1ab2bdb3b72" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.4.0" process: dependency: transitive description: @@ -1301,18 +1301,18 @@ packages: dependency: transitive description: name: sqlite3_web - sha256: "51fec34757577841cc72d79086067e3651c434669d5af557a5c106787198a76f" + sha256: b4043336e74cac54d3ca44c90434a3c310550b9a80851b09ad1af282af0df6d4 url: "https://pub.dev" source: hosted - version: "0.1.2-wip" + version: "0.1.3" sqlite_async: dependency: "direct main" description: name: sqlite_async - sha256: "79e636c857ed43f6cd5e5be72b36967a29f785daa63ff5b078bd34f74f44cb54" + sha256: c5c57b025133d0869cce6a647f99b378ab42cc26488ff22ff942ae9588201af0 url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.9.0" sqlparser: dependency: transitive description: @@ -1650,5 +1650,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index d1bcf7af8..aaee00e47 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,7 +71,7 @@ dependencies: freezed_annotation: ^2.4.4 clock: ^1.1.1 flutter_svg_icons: ^0.0.1 - sqlite_async: ^0.8.1 + sqlite_async: ^0.9.0 logging: ^1.2.0 dependency_overrides: From 3daf2f1bdf83a830ee79bb5300910c6203d75c38 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Thu, 17 Oct 2024 14:53:39 +0300 Subject: [PATCH 16/17] ivm aliases --- lib/models/schema.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/schema.dart b/lib/models/schema.dart index 551e1a019..0b5b72c76 100644 --- a/lib/models/schema.dart +++ b/lib/models/schema.dart @@ -16,10 +16,10 @@ assumptions: nutrition_ingredientcategory, nutrition_weightunit, nutrition_ingre // User,NutritionPlan,Meal,LogItem,MealItem,Ingredient const tableMuscles = 'exercises_muscle'; -const tableLogItems = 'ivm_nutrition_logitem'; +const tableLogItems = 'nutrition_logitem'; const tableNutritionPlans = 'nutrition_nutritionplan'; -const tableMeals = 'ivm_nutrition_meal'; -const tableMealItems = 'ivm_nutrition_mealitem'; +const tableMeals = 'nutrition_meal'; +const tableMealItems = 'nutrition_mealitem'; Schema schema = const Schema([ Table( From 5c446785574b4baa4494741c63f9f2fa7cf60c15 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Wed, 23 Oct 2024 21:32:47 +0300 Subject: [PATCH 17/17] WIP: use new string id's for nutrition stuff so we can use powersync --- lib/helpers/consts.dart | 2 +- lib/models/nutrition/log.dart | 35 +++++--- lib/models/nutrition/log.g.dart | 6 +- lib/models/nutrition/meal.dart | 16 ++-- lib/models/nutrition/meal.g.dart | 4 +- lib/models/nutrition/meal_item.dart | 8 +- lib/models/nutrition/meal_item.g.dart | 2 +- lib/models/nutrition/nutritional_plan.dart | 23 ++--- lib/models/nutrition/nutritional_plan.g.dart | 3 +- lib/models/schema.dart | 12 ++- lib/powersync.dart | 2 + lib/providers/nutrition.dart | 67 +++++++-------- lib/screens/nutritional_diary_screen.dart | 2 +- lib/screens/nutritional_plan_screen.dart | 2 +- lib/widgets/nutrition/forms.dart | 4 +- test/core/settings_test.mocks.dart | 85 +++++-------------- .../nutrition/nutritional_meal_form_test.dart | 2 +- .../nutritional_meal_form_test.mocks.dart | 85 +++++-------------- .../nutrition/nutritional_plan_form_test.dart | 2 +- .../nutritional_plan_form_test.mocks.dart | 85 +++++-------------- .../nutritional_plans_screen_test.dart | 4 +- test_data/nutritional_plans.dart | 32 +++---- 22 files changed, 182 insertions(+), 301 deletions(-) diff --git a/lib/helpers/consts.dart b/lib/helpers/consts.dart index 2b2ebc7e0..6482ec3a7 100644 --- a/lib/helpers/consts.dart +++ b/lib/helpers/consts.dart @@ -109,7 +109,7 @@ enum EXERCISE_IMAGE_ART_STYLE { } /// Dummy ID for pseudo meals -const PSEUDO_MEAL_ID = -1; +const PSEUDO_MEAL_ID = 'deadbeef'; /// Colors used for muscles const COLOR_MAIN_MUSCLES = Colors.red; diff --git a/lib/models/nutrition/log.dart b/lib/models/nutrition/log.dart index 0d3a61a66..362edf05a 100644 --- a/lib/models/nutrition/log.dart +++ b/lib/models/nutrition/log.dart @@ -17,6 +17,7 @@ */ import 'package:json_annotation/json_annotation.dart'; +import 'package:powersync/powersync.dart'; import 'package:powersync/sqlite3.dart' as sqlite; import 'package:wger/helpers/json.dart'; import 'package:wger/models/nutrition/ingredient.dart'; @@ -25,20 +26,19 @@ import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; import 'package:wger/models/schema.dart'; import 'package:wger/powersync.dart'; -import 'package:wger/providers/nutrition.dart'; part 'log.g.dart'; @JsonSerializable() class Log { @JsonKey(required: true) - int? id; + String? id; @JsonKey(required: false, name: 'meal') - int? mealId; + String? mealId; @JsonKey(required: true, name: 'plan') - int planId; + String planId; @JsonKey(required: true) late DateTime datetime; @@ -81,7 +81,7 @@ class Log { factory Log.fromRow(sqlite.Row row) { return Log( - id: int.parse(row['id']), + id: row['id'], mealId: row['meal_id'], ingredientId: row['ingredient_id'], weightUnitId: row['weight_unit_id'], @@ -107,12 +107,12 @@ class Log { return ingredient.nutritionalValues / (100 / weight); } - static Future> readByMealId(int mealId) async { + static Future> readByMealId(String mealId) async { final results = await db.getAll('SELECT * FROM $tableLogItems WHERE meal_id = ?', [mealId]); return results.map((r) => Log.fromRow(r)).toList(); } - static Future> readByPlanId(int planId) async { + static Future> readByPlanId(String planId) async { final results = await db.getAll('SELECT * FROM $tableLogItems WHERE plan_id = ?', [planId]); return results.map((r) => Log.fromRow(r)).toList(); } @@ -121,10 +121,23 @@ class Log { Future delete() async { await db.execute('DELETE FROM $logItemsTable WHERE id = ?', [id]); } + */ - static Future addPhoto(String photoId, String id) async { - await db.execute('UPDATE $logItemsTable SET photo_id = ? WHERE id = ?', [photoId, id]); + Future log() async { + print('DIETER Log.log called id=$id, planId=$planId'); + await db.execute( + 'INSERT INTO $tableLogItems (id, meal_id, ingredient_id, weight_unit_id, amount, plan_id, datetime, comment) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [ + // generate an id using uuid + uuid.v4(), + mealId, + ingredientId, + weightUnitId, + amount, + planId, + datetime.toIso8601String(), + comment, + ], + ); } -} - */ } diff --git a/lib/models/nutrition/log.g.dart b/lib/models/nutrition/log.g.dart index 72e0a6aeb..11e5eb547 100644 --- a/lib/models/nutrition/log.g.dart +++ b/lib/models/nutrition/log.g.dart @@ -19,12 +19,12 @@ Log _$LogFromJson(Map json) { ], ); return Log( - id: (json['id'] as num?)?.toInt(), - mealId: (json['meal'] as num?)?.toInt(), + id: json['id'] as String?, + mealId: json['meal'] as String?, ingredientId: (json['ingredient'] as num).toInt(), weightUnitId: (json['weight_unit'] as num?)?.toInt(), amount: stringToNum(json['amount'] as String?), - planId: (json['plan'] as num).toInt(), + planId: json['plan'] as String, datetime: DateTime.parse(json['datetime'] as String), comment: json['comment'] as String?, ); diff --git a/lib/models/nutrition/meal.dart b/lib/models/nutrition/meal.dart index 3057220b7..10b6109c8 100644 --- a/lib/models/nutrition/meal.dart +++ b/lib/models/nutrition/meal.dart @@ -33,10 +33,10 @@ part 'meal.g.dart'; @JsonSerializable() class Meal { @JsonKey(required: false) - late int? id; + late String? id; @JsonKey(name: 'plan') - late int planId; + late String planId; @JsonKey(toJson: timeToString, fromJson: stringToTime) TimeOfDay? time; @@ -55,7 +55,7 @@ class Meal { Meal({ this.id, - int? plan, + String? plan, this.time, String? name, List? mealItems, @@ -92,7 +92,7 @@ class Meal { factory Meal.fromRow(sqlite.Row row) { return Meal( - id: int.parse(row['id']), + id: row['id'], plan: row['plan_id'], time: stringToTime(row['time']), name: row['name'], @@ -102,8 +102,8 @@ class Meal { Map toJson() => _$MealToJson(this); Meal copyWith({ - int? id, - int? plan, + String? id, + String? plan, TimeOfDay? time, String? name, List? mealItems, @@ -127,12 +127,12 @@ class Meal { ); } - static Future read(int id) async { + static Future read(String id) async { final results = await db.get('SELECT * FROM $tableMeals WHERE id = ?', [id]); return Meal.fromRow(results); } - static Future> readByPlanId(int planId) async { + static Future> readByPlanId(String planId) async { print('Meal.readByPlanId: SELECT * FROM $tableMeals WHERE plan_id = $planId'); final results = await db.getAll('SELECT * FROM $tableMeals WHERE plan_id = ?', [planId]); print(results.rows.length); diff --git a/lib/models/nutrition/meal.g.dart b/lib/models/nutrition/meal.g.dart index 2258775c8..187537b4d 100644 --- a/lib/models/nutrition/meal.g.dart +++ b/lib/models/nutrition/meal.g.dart @@ -7,10 +7,10 @@ part of 'meal.dart'; // ************************************************************************** Meal _$MealFromJson(Map json) => Meal( - id: (json['id'] as num?)?.toInt(), + id: json['id'] as String?, time: stringToTime(json['time'] as String?), name: json['name'] as String?, - )..planId = (json['plan'] as num).toInt(); + )..planId = json['plan'] as String; Map _$MealToJson(Meal instance) => { 'id': instance.id, diff --git a/lib/models/nutrition/meal_item.dart b/lib/models/nutrition/meal_item.dart index 811f86e99..e740d4c2c 100644 --- a/lib/models/nutrition/meal_item.dart +++ b/lib/models/nutrition/meal_item.dart @@ -34,7 +34,7 @@ class MealItem { int? id; @JsonKey(required: false, name: 'meal') - late int mealId; + late String mealId; @JsonKey(required: false, name: 'ingredient') late int ingredientId; @@ -53,7 +53,7 @@ class MealItem { MealItem({ this.id, - int? mealId, + String? mealId, required this.ingredientId, this.weightUnitId, required this.amount, @@ -107,7 +107,7 @@ class MealItem { MealItem copyWith({ int? id, - int? mealId, + String? mealId, int? ingredientId, int? weightUnitId, num? amount, @@ -126,7 +126,7 @@ class MealItem { return m; } - static Future> readByMealId(int mealId) async { + static Future> readByMealId(String mealId) async { final results = await db.getAll('SELECT * FROM $tableMealItems WHERE meal_id = ?', [mealId]); return results.map((r) => MealItem.fromRow(r)).toList(); } diff --git a/lib/models/nutrition/meal_item.g.dart b/lib/models/nutrition/meal_item.g.dart index 256f9cce7..e503d3fc9 100644 --- a/lib/models/nutrition/meal_item.g.dart +++ b/lib/models/nutrition/meal_item.g.dart @@ -13,7 +13,7 @@ MealItem _$MealItemFromJson(Map json) { ); return MealItem( id: (json['id'] as num?)?.toInt(), - mealId: (json['meal'] as num?)?.toInt(), + mealId: json['meal'] as String?, ingredientId: (json['ingredient'] as num).toInt(), weightUnitId: (json['weight_unit'] as num?)?.toInt(), amount: stringToNum(json['amount'] as String?), diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index 213fdfd4b..a8cbbef64 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -35,8 +35,8 @@ part 'nutritional_plan.g.dart'; @JsonSerializable(explicitToJson: true) class NutritionalPlan { - @JsonKey(required: true) - int? id; + @JsonKey(required: false) + String? id; @JsonKey(required: true) late String description; @@ -87,7 +87,7 @@ class NutritionalPlan { factory NutritionalPlan.fromRow(sqlite.Row row) { return NutritionalPlan( - id: int.parse(row['id']), + id: row['id'], description: row['description'], creationDate: DateTime.parse(row['creation_date']), onlyLogging: row['only_logging'] == 1, @@ -100,7 +100,7 @@ class NutritionalPlan { } NutritionalPlan copyWith({ - int? id, + String? id, String? description, DateTime? creationDate, bool? onlyLogging, @@ -299,7 +299,7 @@ class NutritionalPlan { ); } - static Future read(int id) async { + static Future read(String id) async { final row = await db.get('SELECT * FROM $tableNutritionPlans WHERE id = ?', [id]); return NutritionalPlan.fromRow(row).loadChildren(); } @@ -327,18 +327,21 @@ class NutritionalPlan { }); } - static Stream watchNutritionPlan(int id) { + static Stream watchNutritionPlan(String id) { return db.onChange([tableNutritionPlans, tableLogItems, tableMeals]).asyncMap((event) async { final row = await db.getOptional('SELECT * FROM $tableNutritionPlans WHERE id = ?', [id]); return row == null ? null : NutritionalPlan.fromRow(row).loadChildren(); }); } - static Stream watchNutritionPlanLast() { + static Stream watchNutritionPlanLast() { return db.onChange([tableNutritionPlans, tableLogItems, tableMeals]).asyncMap((event) async { - final row = - await db.get('SELECT * FROM $tableNutritionPlans ORDER BY creation_date DESC LIMIT 1'); - return NutritionalPlan.fromRow(row).loadChildren(); + final res = + await db.getAll('SELECT * FROM $tableNutritionPlans ORDER BY creation_date DESC LIMIT 1'); + if (res.isEmpty) { + return null; + } + return NutritionalPlan.fromRow(res.first).loadChildren(); }); } /* diff --git a/lib/models/nutrition/nutritional_plan.g.dart b/lib/models/nutrition/nutritional_plan.g.dart index b4a8bbe73..934459424 100644 --- a/lib/models/nutrition/nutritional_plan.g.dart +++ b/lib/models/nutrition/nutritional_plan.g.dart @@ -10,7 +10,6 @@ NutritionalPlan _$NutritionalPlanFromJson(Map json) { $checkKeys( json, requiredKeys: const [ - 'id', 'description', 'creation_date', 'only_logging', @@ -22,7 +21,7 @@ NutritionalPlan _$NutritionalPlanFromJson(Map json) { ], ); return NutritionalPlan( - id: (json['id'] as num?)?.toInt(), + id: json['id'] as String?, description: json['description'] as String, creationDate: DateTime.parse(json['creation_date'] as String), onlyLogging: json['only_logging'] as bool? ?? false, diff --git a/lib/models/schema.dart b/lib/models/schema.dart index 0b5b72c76..08ecd01b2 100644 --- a/lib/models/schema.dart +++ b/lib/models/schema.dart @@ -33,6 +33,7 @@ Schema schema = const Schema([ Column.text('description'), Column.integer('has_goal_calories'), Column.integer('user_id'), + Column.integer('remote_id'), Column.integer('only_logging'), Column.integer('goal_carbohydrates'), Column.integer('goal_energy'), @@ -47,10 +48,11 @@ Schema schema = const Schema([ Column.text('datetime'), Column.text('comment'), Column.integer('amount'), + Column.integer('remote_id'), Column.integer('ingredient_id'), - Column.integer('plan_id'), + Column.text('plan_id'), Column.integer('weight_unit_id'), - Column.integer('meal_id'), // optional + Column.text('meal_id'), // optional ], indexes: [ // Index('plan', [IndexedColumn('plan_id')]) @@ -60,8 +62,9 @@ Schema schema = const Schema([ tableMeals, [ Column.integer('order'), + Column.integer('remote_id'), Column.text('time'), - Column.integer('plan_id'), + Column.text('plan_id'), Column.text('name'), ], ), @@ -71,7 +74,8 @@ Schema schema = const Schema([ Column.integer('order'), Column.integer('amount'), Column.integer('ingredient_id'), - Column.integer('meal_id'), + Column.text('meal_id'), + Column.integer('remote_id'), Column.integer('weight_unit_id'), ], ), diff --git a/lib/powersync.dart b/lib/powersync.dart index 4532ae0f7..0a2122c97 100644 --- a/lib/powersync.dart +++ b/lib/powersync.dart @@ -60,6 +60,8 @@ class DjangoConnector extends PowerSyncBackendConnector { 'data': {'id': op.id, ...?op.opData}, }; + log.fine('DIETER Uploading record', record); + switch (op.op) { case UpdateType.put: await apiClient.upsert(record); diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index a092a6639..62c8ef3f3 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -74,13 +74,14 @@ class NutritionPlansProvider with ChangeNotifier { } return null; } - +/* NutritionalPlan findById(int id) { return _plans.firstWhere( (plan) => plan.id == id, orElse: () => throw const NoSuchEntryException(), ); } + Meal? findMealById(int id) { for (final plan in _plans) { @@ -91,6 +92,7 @@ class NutritionPlansProvider with ChangeNotifier { } return null; } + */ Future _enrichPlan(NutritionalPlan plan) async { // TODO: set up ingredient images @@ -119,7 +121,7 @@ class NutritionPlansProvider with ChangeNotifier { return plan; } - Stream watchNutritionPlan(int id) { + Stream watchNutritionPlan(String id) { return NutritionalPlan.watchNutritionPlan(id).transform( StreamTransformer.fromHandlers( handleData: (plan, sink) async { @@ -137,6 +139,9 @@ class NutritionPlansProvider with ChangeNotifier { return NutritionalPlan.watchNutritionPlanLast().transform( StreamTransformer.fromHandlers( handleData: (plan, sink) async { + if (plan == null) { + return; + } sink.add(await _enrichPlan(plan)); }, ), @@ -172,31 +177,16 @@ TODO implement: } Future editPlan(NutritionalPlan plan) async { - await baseProvider.patch( - plan.toJson(), - baseProvider.makeUrl(_nutritionalPlansPath, id: plan.id), - ); - notifyListeners(); +// TODO } - Future deletePlan(int id) async { - final existingPlanIndex = _plans.indexWhere((element) => element.id == id); - final existingPlan = _plans[existingPlanIndex]; - _plans.removeAt(existingPlanIndex); - notifyListeners(); - - final response = await baseProvider.deleteRequest(_nutritionalPlansPath, id); - - if (response.statusCode >= 400) { - _plans.insert(existingPlanIndex, existingPlan); - notifyListeners(); - throw WgerHttpException(response.body); - } - //existingPlan = null; + Future deletePlan(String id) async { +// TODO } /// Adds a meal to a plan - Future addMeal(Meal meal, int planId) async { + Future addMeal(Meal meal, String planId) async { + /* final plan = findById(planId); final data = await baseProvider.post( meal.toJson(), @@ -208,10 +198,13 @@ TODO implement: notifyListeners(); return meal; + */ + return meal; } /// Edits an existing meal Future editMeal(Meal meal) async { + /* final data = await baseProvider.patch( meal.toJson(), baseProvider.makeUrl(_mealPath, id: meal.id), @@ -219,11 +212,13 @@ TODO implement: meal = Meal.fromJson(data); notifyListeners(); + */ return meal; } /// Deletes a meal Future deleteMeal(Meal meal) async { + /* // Get the meal final plan = findById(meal.planId); final mealIndex = plan.meals.indexWhere((e) => e.id == meal.id); @@ -238,6 +233,8 @@ TODO implement: notifyListeners(); throw WgerHttpException(response.body); } + */ + return; } /// Adds a meal item to a meal @@ -257,6 +254,7 @@ TODO implement: /// Deletes a meal Future deleteMealItem(MealItem mealItem) async { + /* // Get the meal final meal = findMealById(mealItem.mealId)!; final mealItemIndex = meal.mealItems.indexWhere((e) => e.id == mealItem.id); @@ -271,6 +269,7 @@ TODO implement: notifyListeners(); throw WgerHttpException(response.body); } + */ } Future clearIngredientCache() async { @@ -380,6 +379,7 @@ TODO implement: /// Log meal to nutrition diary Future logMealToDiary(Meal meal) async { + /* for (final item in meal.mealItems) { final plan = findById(meal.planId); final Log log = Log.fromMealItem(item, plan.id!, meal.id); @@ -392,34 +392,29 @@ TODO implement: plan.diaryEntries.add(log); } notifyListeners(); + */ } /// Log custom ingredient to nutrition diary Future logIngredientToDiary( MealItem mealItem, - int planId, [ + String planId, [ DateTime? dateTime, - ]) async { - final plan = findById(planId); - mealItem.ingredient = await fetchIngredient(mealItem.ingredientId); - final Log log = Log.fromMealItem(mealItem, plan.id!, null, dateTime); - - final data = await baseProvider.post( - log.toJson(), - baseProvider.makeUrl(_nutritionDiaryPath), - ); - log.id = data['id']; - plan.diaryEntries.add(log); - notifyListeners(); + ]) { + print( + 'DIETER logIngredientToDiary called ingredient=${mealItem.ingredientId}, planId=$planId, dateTime=$dateTime'); + return Log.fromMealItem(mealItem, planId, null, dateTime).log(); } /// Deletes a log entry - Future deleteLog(int logId, int planId) async { + Future deleteLog(String logId, String planId) async { + /* await baseProvider.deleteRequest(_nutritionDiaryPath, logId); final plan = findById(planId); plan.diaryEntries.removeWhere((element) => element.id == logId); notifyListeners(); + */ } /// Load nutrition diary entries for plan diff --git a/lib/screens/nutritional_diary_screen.dart b/lib/screens/nutritional_diary_screen.dart index 1ea1c15c4..72d97daf8 100644 --- a/lib/screens/nutritional_diary_screen.dart +++ b/lib/screens/nutritional_diary_screen.dart @@ -28,7 +28,7 @@ import 'package:wger/widgets/nutrition/nutritional_diary_detail.dart'; /// Arguments passed to the form screen class NutritionalDiaryArguments { /// Nutritional plan - final int plan; + final String plan; /// Date to show data for final DateTime date; diff --git a/lib/screens/nutritional_plan_screen.dart b/lib/screens/nutritional_plan_screen.dart index 6201433d8..4f1c5ec95 100644 --- a/lib/screens/nutritional_plan_screen.dart +++ b/lib/screens/nutritional_plan_screen.dart @@ -49,7 +49,7 @@ class _NutritionalPlanScreenState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - final id = ModalRoute.of(context)!.settings.arguments as int; + final id = ModalRoute.of(context)!.settings.arguments as String; //final id = 111; final stream = diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 785e9bacb..faad4a6f9 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -36,7 +36,7 @@ import 'package:wger/widgets/nutrition/widgets.dart'; class MealForm extends StatelessWidget { late final Meal _meal; - final int _planId; + final String _planId; final _form = GlobalKey(); final _timeController = TextEditingController(); @@ -128,7 +128,7 @@ Widget MealItemForm( ]) { return IngredientForm( // TODO we use planId 0 here cause we don't have one and we don't need it I think? - recent: recent.map((e) => Log.fromMealItem(e, 0, e.mealId)).toList(), + recent: recent.map((e) => Log.fromMealItem(e, "0", e.mealId)).toList(), onSave: (BuildContext context, MealItem mealItem, DateTime? dt) { mealItem.mealId = meal.id!; Provider.of(context, listen: false).addMealItem(mealItem, meal); diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index 303216098..5812e036b 100644 --- a/test/core/settings_test.mocks.dart +++ b/test/core/settings_test.mocks.dart @@ -711,79 +711,34 @@ class MockNutritionPlansProvider extends _i1.Mock ); @override - _i10.NutritionalPlan findById(int? id) => (super.noSuchMethod( + _i15.Stream<_i10.NutritionalPlan?> watchNutritionPlan(String? id) => + (super.noSuchMethod( Invocation.method( - #findById, + #watchNutritionPlan, [id], ), - returnValue: _FakeNutritionalPlan_8( - this, - Invocation.method( - #findById, - [id], - ), - ), - ) as _i10.NutritionalPlan); - - @override - _i11.Meal? findMealById(int? id) => (super.noSuchMethod(Invocation.method( - #findMealById, - [id], - )) as _i11.Meal?); - - @override - _i15.Future fetchAndSetAllPlansSparse() => (super.noSuchMethod( - Invocation.method( - #fetchAndSetAllPlansSparse, - [], - ), - returnValue: _i15.Future.value(), - returnValueForMissingStub: _i15.Future.value(), - ) as _i15.Future); - - @override - _i15.Future fetchAndSetAllPlansFull() => (super.noSuchMethod( - Invocation.method( - #fetchAndSetAllPlansFull, - [], - ), - returnValue: _i15.Future.value(), - returnValueForMissingStub: _i15.Future.value(), - ) as _i15.Future); + returnValue: _i15.Stream<_i10.NutritionalPlan?>.empty(), + ) as _i15.Stream<_i10.NutritionalPlan?>); @override - _i15.Future<_i10.NutritionalPlan> fetchAndSetPlanSparse(int? planId) => + _i15.Stream<_i10.NutritionalPlan> watchNutritionPlanLast() => (super.noSuchMethod( Invocation.method( - #fetchAndSetPlanSparse, - [planId], + #watchNutritionPlanLast, + [], ), - returnValue: - _i15.Future<_i10.NutritionalPlan>.value(_FakeNutritionalPlan_8( - this, - Invocation.method( - #fetchAndSetPlanSparse, - [planId], - ), - )), - ) as _i15.Future<_i10.NutritionalPlan>); + returnValue: _i15.Stream<_i10.NutritionalPlan>.empty(), + ) as _i15.Stream<_i10.NutritionalPlan>); @override - _i15.Future<_i10.NutritionalPlan> fetchAndSetPlanFull(int? planId) => + _i15.Stream> watchNutritionPlans() => (super.noSuchMethod( Invocation.method( - #fetchAndSetPlanFull, - [planId], + #watchNutritionPlans, + [], ), - returnValue: - _i15.Future<_i10.NutritionalPlan>.value(_FakeNutritionalPlan_8( - this, - Invocation.method( - #fetchAndSetPlanFull, - [planId], - ), - )), - ) as _i15.Future<_i10.NutritionalPlan>); + returnValue: _i15.Stream>.empty(), + ) as _i15.Stream>); @override _i15.Future<_i10.NutritionalPlan> addPlan(_i10.NutritionalPlan? planData) => @@ -813,7 +768,7 @@ class MockNutritionPlansProvider extends _i1.Mock ) as _i15.Future); @override - _i15.Future deletePlan(int? id) => (super.noSuchMethod( + _i15.Future deletePlan(String? id) => (super.noSuchMethod( Invocation.method( #deletePlan, [id], @@ -825,7 +780,7 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i15.Future<_i11.Meal> addMeal( _i11.Meal? meal, - int? planId, + String? planId, ) => (super.noSuchMethod( Invocation.method( @@ -991,7 +946,7 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i15.Future logIngredientToDiary( _i12.MealItem? mealItem, - int? planId, [ + String? planId, [ DateTime? dateTime, ]) => (super.noSuchMethod( @@ -1009,8 +964,8 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i15.Future deleteLog( - int? logId, - int? planId, + String? logId, + String? planId, ) => (super.noSuchMethod( Invocation.method( diff --git a/test/nutrition/nutritional_meal_form_test.dart b/test/nutrition/nutritional_meal_form_test.dart index fdb781f64..a053b19fa 100644 --- a/test/nutrition/nutritional_meal_form_test.dart +++ b/test/nutrition/nutritional_meal_form_test.dart @@ -60,7 +60,7 @@ void main() { localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, navigatorKey: key, - home: Scaffold(body: MealForm(1, meal)), + home: Scaffold(body: MealForm("1", meal)), routes: { NutritionalPlanScreen.routeName: (ctx) => const NutritionalPlanScreen(), }, diff --git a/test/nutrition/nutritional_meal_form_test.mocks.dart b/test/nutrition/nutritional_meal_form_test.mocks.dart index 3df6d7b22..08ce46e60 100644 --- a/test/nutrition/nutritional_meal_form_test.mocks.dart +++ b/test/nutrition/nutritional_meal_form_test.mocks.dart @@ -165,79 +165,34 @@ class MockNutritionPlansProvider extends _i1.Mock ); @override - _i4.NutritionalPlan findById(int? id) => (super.noSuchMethod( + _i9.Stream<_i4.NutritionalPlan?> watchNutritionPlan(String? id) => + (super.noSuchMethod( Invocation.method( - #findById, + #watchNutritionPlan, [id], ), - returnValue: _FakeNutritionalPlan_2( - this, - Invocation.method( - #findById, - [id], - ), - ), - ) as _i4.NutritionalPlan); - - @override - _i5.Meal? findMealById(int? id) => (super.noSuchMethod(Invocation.method( - #findMealById, - [id], - )) as _i5.Meal?); - - @override - _i9.Future fetchAndSetAllPlansSparse() => (super.noSuchMethod( - Invocation.method( - #fetchAndSetAllPlansSparse, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i9.Stream<_i4.NutritionalPlan?>.empty(), + ) as _i9.Stream<_i4.NutritionalPlan?>); @override - _i9.Future fetchAndSetAllPlansFull() => (super.noSuchMethod( - Invocation.method( - #fetchAndSetAllPlansFull, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future<_i4.NutritionalPlan> fetchAndSetPlanSparse(int? planId) => + _i9.Stream<_i4.NutritionalPlan> watchNutritionPlanLast() => (super.noSuchMethod( Invocation.method( - #fetchAndSetPlanSparse, - [planId], + #watchNutritionPlanLast, + [], ), - returnValue: - _i9.Future<_i4.NutritionalPlan>.value(_FakeNutritionalPlan_2( - this, - Invocation.method( - #fetchAndSetPlanSparse, - [planId], - ), - )), - ) as _i9.Future<_i4.NutritionalPlan>); + returnValue: _i9.Stream<_i4.NutritionalPlan>.empty(), + ) as _i9.Stream<_i4.NutritionalPlan>); @override - _i9.Future<_i4.NutritionalPlan> fetchAndSetPlanFull(int? planId) => + _i9.Stream> watchNutritionPlans() => (super.noSuchMethod( Invocation.method( - #fetchAndSetPlanFull, - [planId], + #watchNutritionPlans, + [], ), - returnValue: - _i9.Future<_i4.NutritionalPlan>.value(_FakeNutritionalPlan_2( - this, - Invocation.method( - #fetchAndSetPlanFull, - [planId], - ), - )), - ) as _i9.Future<_i4.NutritionalPlan>); + returnValue: _i9.Stream>.empty(), + ) as _i9.Stream>); @override _i9.Future<_i4.NutritionalPlan> addPlan(_i4.NutritionalPlan? planData) => @@ -267,7 +222,7 @@ class MockNutritionPlansProvider extends _i1.Mock ) as _i9.Future); @override - _i9.Future deletePlan(int? id) => (super.noSuchMethod( + _i9.Future deletePlan(String? id) => (super.noSuchMethod( Invocation.method( #deletePlan, [id], @@ -279,7 +234,7 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i9.Future<_i5.Meal> addMeal( _i5.Meal? meal, - int? planId, + String? planId, ) => (super.noSuchMethod( Invocation.method( @@ -445,7 +400,7 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i9.Future logIngredientToDiary( _i6.MealItem? mealItem, - int? planId, [ + String? planId, [ DateTime? dateTime, ]) => (super.noSuchMethod( @@ -463,8 +418,8 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i9.Future deleteLog( - int? logId, - int? planId, + String? logId, + String? planId, ) => (super.noSuchMethod( Invocation.method( diff --git a/test/nutrition/nutritional_plan_form_test.dart b/test/nutrition/nutritional_plan_form_test.dart index e366ef51f..ce89c0fef 100644 --- a/test/nutrition/nutritional_plan_form_test.dart +++ b/test/nutrition/nutritional_plan_form_test.dart @@ -35,7 +35,7 @@ void main() { var mockNutrition = MockNutritionPlansProvider(); final plan1 = NutritionalPlan( - id: 1, + id: 'deadbeef', creationDate: DateTime(2021, 1, 1), description: 'test plan 1', ); diff --git a/test/nutrition/nutritional_plan_form_test.mocks.dart b/test/nutrition/nutritional_plan_form_test.mocks.dart index 3b48c5305..735d15379 100644 --- a/test/nutrition/nutritional_plan_form_test.mocks.dart +++ b/test/nutrition/nutritional_plan_form_test.mocks.dart @@ -165,79 +165,34 @@ class MockNutritionPlansProvider extends _i1.Mock ); @override - _i4.NutritionalPlan findById(int? id) => (super.noSuchMethod( + _i9.Stream<_i4.NutritionalPlan?> watchNutritionPlan(String? id) => + (super.noSuchMethod( Invocation.method( - #findById, + #watchNutritionPlan, [id], ), - returnValue: _FakeNutritionalPlan_2( - this, - Invocation.method( - #findById, - [id], - ), - ), - ) as _i4.NutritionalPlan); - - @override - _i5.Meal? findMealById(int? id) => (super.noSuchMethod(Invocation.method( - #findMealById, - [id], - )) as _i5.Meal?); - - @override - _i9.Future fetchAndSetAllPlansSparse() => (super.noSuchMethod( - Invocation.method( - #fetchAndSetAllPlansSparse, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i9.Stream<_i4.NutritionalPlan?>.empty(), + ) as _i9.Stream<_i4.NutritionalPlan?>); @override - _i9.Future fetchAndSetAllPlansFull() => (super.noSuchMethod( - Invocation.method( - #fetchAndSetAllPlansFull, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future<_i4.NutritionalPlan> fetchAndSetPlanSparse(int? planId) => + _i9.Stream<_i4.NutritionalPlan> watchNutritionPlanLast() => (super.noSuchMethod( Invocation.method( - #fetchAndSetPlanSparse, - [planId], + #watchNutritionPlanLast, + [], ), - returnValue: - _i9.Future<_i4.NutritionalPlan>.value(_FakeNutritionalPlan_2( - this, - Invocation.method( - #fetchAndSetPlanSparse, - [planId], - ), - )), - ) as _i9.Future<_i4.NutritionalPlan>); + returnValue: _i9.Stream<_i4.NutritionalPlan>.empty(), + ) as _i9.Stream<_i4.NutritionalPlan>); @override - _i9.Future<_i4.NutritionalPlan> fetchAndSetPlanFull(int? planId) => + _i9.Stream> watchNutritionPlans() => (super.noSuchMethod( Invocation.method( - #fetchAndSetPlanFull, - [planId], + #watchNutritionPlans, + [], ), - returnValue: - _i9.Future<_i4.NutritionalPlan>.value(_FakeNutritionalPlan_2( - this, - Invocation.method( - #fetchAndSetPlanFull, - [planId], - ), - )), - ) as _i9.Future<_i4.NutritionalPlan>); + returnValue: _i9.Stream>.empty(), + ) as _i9.Stream>); @override _i9.Future<_i4.NutritionalPlan> addPlan(_i4.NutritionalPlan? planData) => @@ -267,7 +222,7 @@ class MockNutritionPlansProvider extends _i1.Mock ) as _i9.Future); @override - _i9.Future deletePlan(int? id) => (super.noSuchMethod( + _i9.Future deletePlan(String? id) => (super.noSuchMethod( Invocation.method( #deletePlan, [id], @@ -279,7 +234,7 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i9.Future<_i5.Meal> addMeal( _i5.Meal? meal, - int? planId, + String? planId, ) => (super.noSuchMethod( Invocation.method( @@ -445,7 +400,7 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i9.Future logIngredientToDiary( _i6.MealItem? mealItem, - int? planId, [ + String? planId, [ DateTime? dateTime, ]) => (super.noSuchMethod( @@ -463,8 +418,8 @@ class MockNutritionPlansProvider extends _i1.Mock @override _i9.Future deleteLog( - int? logId, - int? planId, + String? logId, + String? planId, ) => (super.noSuchMethod( Invocation.method( diff --git a/test/nutrition/nutritional_plans_screen_test.dart b/test/nutrition/nutritional_plans_screen_test.dart index 5a60b2503..eea8de89a 100644 --- a/test/nutrition/nutritional_plans_screen_test.dart +++ b/test/nutrition/nutritional_plans_screen_test.dart @@ -67,12 +67,12 @@ void main() { mockBaseProvider, [ NutritionalPlan( - id: 1, + id: 'deadbeefa', description: 'test plan 1', creationDate: DateTime(2021, 01, 01), ), NutritionalPlan( - id: 2, + id: 'deadbeefb', description: 'test plan 2', creationDate: DateTime(2021, 01, 10), ), diff --git a/test_data/nutritional_plans.dart b/test_data/nutritional_plans.dart index 1b3a42cc6..9a202bc4c 100644 --- a/test_data/nutritional_plans.dart +++ b/test_data/nutritional_plans.dart @@ -156,32 +156,32 @@ NutritionalPlan getNutritionalPlan() { mealItem3.ingredient = ingredient3; final meal1 = Meal( - id: 1, - plan: 1, + id: 'deadbeefa', + plan: '1', time: const TimeOfDay(hour: 17, minute: 0), name: 'Initial Name 1', ); meal1.mealItems = [mealItem1, mealItem2]; final meal2 = Meal( - id: 2, - plan: 1, + id: 'deadbeefb', + plan: '1', time: const TimeOfDay(hour: 22, minute: 5), name: 'Initial Name 2', ); meal2.mealItems = [mealItem3]; final NutritionalPlan plan = NutritionalPlan( - id: 1, + id: 'deadbeefc', description: 'Less fat, more protein', creationDate: DateTime(2021, 5, 23), ); plan.meals = [meal1, meal2]; // Add logs - plan.diaryEntries.add(Log.fromMealItem(mealItem1, 1, 1, DateTime(2021, 6, 1))); - plan.diaryEntries.add(Log.fromMealItem(mealItem2, 1, 1, DateTime(2021, 6, 1))); - plan.diaryEntries.add(Log.fromMealItem(mealItem3, 1, 1, DateTime(2021, 6, 10))); + plan.diaryEntries.add(Log.fromMealItem(mealItem1, '1', '1', DateTime(2021, 6, 1))); + plan.diaryEntries.add(Log.fromMealItem(mealItem2, '1', '1', DateTime(2021, 6, 1))); + plan.diaryEntries.add(Log.fromMealItem(mealItem3, '1', '1', DateTime(2021, 6, 10))); return plan; } @@ -194,32 +194,32 @@ NutritionalPlan getNutritionalPlanScreenshot() { final mealItem3 = MealItem(ingredientId: 3, amount: 100, ingredient: apple); final meal1 = Meal( - id: 1, - plan: 1, + id: 'deadbeefa', + plan: '1', time: const TimeOfDay(hour: 8, minute: 30), name: 'Breakfast', mealItems: [mealItem1, mealItem2], ); final meal2 = Meal( - id: 2, - plan: 1, + id: 'deadbeefb', + plan: '1', time: const TimeOfDay(hour: 11, minute: 0), name: 'Snack 1', mealItems: [mealItem3], ); final NutritionalPlan plan = NutritionalPlan( - id: 1, + id: '1', description: 'Diet', creationDate: DateTime(2021, 5, 23), meals: [meal1, meal2], ); // Add logs - plan.diaryEntries.add(Log.fromMealItem(mealItem1, 1, 1, DateTime.now())); - plan.diaryEntries.add(Log.fromMealItem(mealItem2, 1, 1, DateTime.now())); - plan.diaryEntries.add(Log.fromMealItem(mealItem3, 1, 1, DateTime.now())); + plan.diaryEntries.add(Log.fromMealItem(mealItem1, '1', '1', DateTime.now())); + plan.diaryEntries.add(Log.fromMealItem(mealItem2, '1', '1', DateTime.now())); + plan.diaryEntries.add(Log.fromMealItem(mealItem3, '1', '1', DateTime.now())); for (final i in plan.diaryEntries) { i.datetime = DateTime.now();