Use the powersync drift provider to show exercises

This commit is contained in:
Roland Geider
2025-10-24 20:52:02 +02:00
parent 7969376dce
commit 666359155d
10 changed files with 1564 additions and 22 deletions

View File

@@ -11,10 +11,14 @@ import 'package:wger/models/exercises/language.dart';
import 'package:wger/models/exercises/muscle.dart';
import 'package:wger/models/exercises/translation.dart';
import 'package:wger/models/exercises/video.dart';
import 'package:wger/models/measurements/measurement_category.dart';
import 'package:wger/models/workouts/log.dart';
import 'powersync.dart';
import 'tables/exercise.dart';
import 'tables/language.dart';
import 'tables/measurements.dart';
import 'tables/routines.dart';
import 'tables/weight.dart';
part 'database.g.dart';
@@ -37,6 +41,8 @@ part 'database.g.dart';
// User data
WeightEntryTable,
MeasurementCategoryTable,
WorkoutLogTable,
],
//include: {'queries.drift'},
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
import 'package:drift/drift.dart';
import 'package:wger/models/measurements/measurement_category.dart';
import 'package:wger/models/measurements/measurement_entry.dart';
@UseRowClass(MeasurementCategory)
class MeasurementCategoryTable extends Table {
@override
String get tableName => 'measurements_category';
IntColumn get id => integer()();
TextColumn get name => text()();
TextColumn get unit => text()();
}
@UseRowClass(MeasurementEntry)
class MeasurementEntryTable extends Table {
@override
String get tableName => 'measurements_measurement';
IntColumn get id => integer()();
DateTimeColumn get date => dateTime()();
RealColumn get value => real()();
TextColumn get notes => text()();
}

View File

@@ -0,0 +1,29 @@
import 'package:drift/drift.dart';
import 'package:powersync/powersync.dart' show uuid;
import 'package:wger/models/workouts/log.dart';
@UseRowClass(Log)
class WorkoutLogTable extends Table {
@override
String get tableName => 'manager_workoutlog';
TextColumn get id => text().clientDefault(() => uuid.v4())();
IntColumn get exerciseId => integer().named('exercise_id')();
IntColumn get routineId => integer().named('routine_id')();
IntColumn get sessionId => integer().named('session_id').nullable()();
IntColumn get iteration => integer().nullable()();
IntColumn get slotEntryId => integer().named('slot_entry_id').nullable()();
RealColumn get rir => real().nullable()();
RealColumn get rirTarget => real().named('rir_target').nullable()();
RealColumn get repetitions => real()();
RealColumn get repetitionsTarget => real().named('repetitions_target')();
IntColumn get repetitionsUnitId => integer().named('repetitions_unit_id').nullable()();
RealColumn get weight => real()();
RealColumn get weightTarget => real().named('weight_target')();
IntColumn get weightUnitId => integer().named('weight_unit_id').nullable()();
DateTimeColumn get date => dateTime()();
}

View File

@@ -296,7 +296,7 @@ void showDeleteDialog(BuildContext context, String confirmDeleteName, Log log) a
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
onPressed: () {
context.read<RoutinesProvider>().deleteLog(log.id!, log.routineId);
context.read<RoutinesProvider>().deleteLog(log.id.toString(), log.routineId);
Navigator.of(contextDialog).pop();

View File

@@ -95,6 +95,9 @@ Schema schema = const Schema([
Column.text('url'),
Column.integer('is_main'),
],
indexes: [
Index('exercise', [IndexedColumn('exercise_id')]),
],
),
Table(
'exercises_exercisevideo',
@@ -111,6 +114,9 @@ Schema schema = const Schema([
Column.integer('license_id'),
Column.text('license_author'),
],
indexes: [
Index('exercise', [IndexedColumn('exercise_id')]),
],
),
//
@@ -124,4 +130,38 @@ Schema schema = const Schema([
Column.text('date'),
],
),
Table(
'measurements_category',
[
Column.text('name'),
Column.real('unit'),
],
),
Table(
'manager_workoutlog',
[
Column.integer('exercise_id'),
Column.integer('routine_id'),
Column.integer('session_id'),
Column.integer('iteration'),
Column.integer('slot_entry_id'),
Column.real('rir'),
Column.real('rir_target'),
Column.real('repetitions'),
Column.real('repetitions_target'),
Column.integer('repetitions_unit_id'),
Column.real('weight'),
Column.real('weight_target'),
Column.integer('weight_unit_id'),
Column.text('date'),
],
indexes: [
Index('exercise', [IndexedColumn('exercise_id')]),
Index('slot_entry', [IndexedColumn('slot_entry_id')]),
Index('routine', [IndexedColumn('routine_id')]),
Index('session', [IndexedColumn('session_id')]),
Index('repetitions_unit', [IndexedColumn('repetitions_unit_id')]),
Index('weight_unit', [IndexedColumn('weight_unit_id')]),
],
),
]);

View File

@@ -4,6 +4,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:wger/database/powersync/database.dart';
import 'package:wger/models/exercises/equipment.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/models/exercises/muscle.dart';
part 'exercise_data.g.dart';
@@ -14,6 +15,7 @@ final class ExerciseNotifier extends _$ExerciseNotifier {
@override
Stream<List<Exercise>> build() {
final db = ref.read(driftPowerSyncDatabase);
_logger.fine('Building exercise stream');
final primaryMuscleTable = db.alias(db.muscleTable, 'pm');
final secondaryMuscleTable = db.alias(db.muscleTable, 'sm');
@@ -25,6 +27,12 @@ final class ExerciseNotifier extends _$ExerciseNotifier {
db.exerciseTranslationTable.exerciseId.equalsExp(db.exerciseTable.id),
),
// Language
leftOuterJoin(
db.languageTable,
db.languageTable.id.equalsExp(db.exerciseTranslationTable.languageId),
),
// Exercise <-> Muscle
leftOuterJoin(
db.exerciseMuscleM2N,
@@ -80,6 +88,7 @@ final class ExerciseNotifier extends _$ExerciseNotifier {
}
if (translation != null && !entry.translations.any((t) => t.id == translation.id)) {
translation.language = row.readTable(db.languageTable);
entry.translations.add(translation);
}
@@ -104,6 +113,21 @@ final class ExerciseNotifier extends _$ExerciseNotifier {
return map.values.toList();
});
}
Exercise? getById(int id) {
// Can be null e.g. during initial loading
final cached = state.asData?.value;
if (cached == null) {
return null;
}
for (final e in cached) {
if (e.id == id) {
return e;
}
}
return null;
}
}
@riverpod
@@ -114,3 +138,12 @@ final class ExerciseEquipmentNotifier extends _$ExerciseEquipmentNotifier {
return db.select(db.equipmentTable).watch();
}
}
@riverpod
final class ExerciseMuscleNotifier extends _$ExerciseMuscleNotifier {
@override
Stream<List<Muscle>> build() {
final db = ref.read(driftPowerSyncDatabase);
return db.select(db.muscleTable).watch();
}
}

View File

@@ -33,7 +33,7 @@ final class ExerciseNotifierProvider
ExerciseNotifier create() => ExerciseNotifier();
}
String _$exerciseNotifierHash() => r'4f6613f81e292dd3c74f9eebbbb20482b8da1e42';
String _$exerciseNotifierHash() => r'2b7dafd181f3659590ddbd961e7f903d6ba31338';
abstract class _$ExerciseNotifier extends $StreamNotifier<List<Exercise>> {
Stream<List<Exercise>> build();
@@ -98,3 +98,48 @@ abstract class _$ExerciseEquipmentNotifier extends $StreamNotifier<List<Equipmen
element.handleValue(ref, created);
}
}
@ProviderFor(ExerciseMuscleNotifier)
const exerciseMuscleProvider = ExerciseMuscleNotifierProvider._();
final class ExerciseMuscleNotifierProvider
extends $StreamNotifierProvider<ExerciseMuscleNotifier, List<Muscle>> {
const ExerciseMuscleNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'exerciseMuscleProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$exerciseMuscleNotifierHash();
@$internal
@override
ExerciseMuscleNotifier create() => ExerciseMuscleNotifier();
}
String _$exerciseMuscleNotifierHash() => r'6be0b7400776f1593194fb43667acbeee8e3a4a8';
abstract class _$ExerciseMuscleNotifier extends $StreamNotifier<List<Muscle>> {
Stream<List<Muscle>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<List<Muscle>>, List<Muscle>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<Muscle>>, List<Muscle>>,
AsyncValue<List<Muscle>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,25 +1,21 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/providers/exercises.dart';
import 'package:wger/providers/exercise_data.dart';
import 'package:wger/widgets/core/app_bar.dart';
import 'package:wger/widgets/core/progress_indicator.dart';
import 'package:wger/widgets/exercises/filter_row.dart';
import 'package:wger/widgets/exercises/list_tile.dart';
class ExercisesScreen extends StatefulWidget {
class ExercisesScreen extends ConsumerWidget {
const ExercisesScreen({super.key});
static const routeName = '/exercises';
@override
_ExercisesScreenState createState() => _ExercisesScreenState();
}
class _ExercisesScreenState extends State<ExercisesScreen> {
@override
Widget build(BuildContext context) {
final exercisesList = Provider.of<ExercisesProvider>(context).filteredExercises;
Widget build(BuildContext context, WidgetRef ref) {
final exercises = ref.watch(exerciseProvider);
return Scaffold(
appBar: EmptyAppBar(AppLocalizations.of(context).exercises),
@@ -27,15 +23,21 @@ class _ExercisesScreenState extends State<ExercisesScreen> {
children: [
const FilterRow(),
Expanded(
child: exercisesList.isEmpty
? const Center(
child: SizedBox(
height: 30,
width: 30,
child: CircularProgressIndicator(),
),
)
: _ExercisesList(exerciseList: exercisesList),
child: exercises.when(
data: (List<Exercise> exercisesList) {
return exercisesList.isEmpty
? const Center(
child: SizedBox(
height: 30,
width: 30,
child: CircularProgressIndicator(),
),
)
: _ExercisesList(exerciseList: exercisesList);
},
loading: () => const BoxedProgressIndicator(),
error: (err, st) => Center(child: Text('Error: $err')),
),
),
],
),

View File

@@ -61,7 +61,7 @@ Widget _getStatusIcon(ps.SyncStatus status) {
} else if (status.downloading) {
return _makeIcon('Downloading', Icons.cloud_download_outlined);
} else {
return _makeIcon('Connected', Icons.cloud_queue);
return _makeIcon('Connected', Icons.cloud_done_outlined);
}
}