mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Add dashboard widget and rework the provider
This commit is contained in:
57
lib/models/trophies/user_trophy.dart
Normal file
57
lib/models/trophies/user_trophy.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
|
||||
import 'trophy.dart';
|
||||
|
||||
part 'user_trophy.g.dart';
|
||||
|
||||
/// A trophy awarded to a user for achieving a specific milestone.
|
||||
|
||||
@JsonSerializable()
|
||||
class UserTrophy {
|
||||
@JsonKey(required: true)
|
||||
final int id;
|
||||
|
||||
@JsonKey(required: true)
|
||||
final Trophy trophy;
|
||||
|
||||
@JsonKey(required: true, name: 'earned_at', fromJson: utcIso8601ToLocalDate)
|
||||
final DateTime earnedAt;
|
||||
|
||||
@JsonKey(required: true)
|
||||
final num progress;
|
||||
|
||||
@JsonKey(required: true, name: 'is_notified')
|
||||
final bool isNotified;
|
||||
|
||||
UserTrophy({
|
||||
required this.id,
|
||||
required this.trophy,
|
||||
required this.earnedAt,
|
||||
required this.progress,
|
||||
required this.isNotified,
|
||||
});
|
||||
|
||||
// Boilerplate
|
||||
factory UserTrophy.fromJson(Map<String, dynamic> json) => _$UserTrophyFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserTrophyToJson(this);
|
||||
}
|
||||
53
lib/models/trophies/user_trophy.g.dart
Normal file
53
lib/models/trophies/user_trophy.g.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user_trophy.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
UserTrophy _$UserTrophyFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const [
|
||||
'id',
|
||||
'trophy',
|
||||
'earned_at',
|
||||
'progress',
|
||||
'is_notified',
|
||||
],
|
||||
);
|
||||
return UserTrophy(
|
||||
id: (json['id'] as num).toInt(),
|
||||
trophy: Trophy.fromJson(json['trophy'] as Map<String, dynamic>),
|
||||
earnedAt: utcIso8601ToLocalDate(json['earned_at'] as String),
|
||||
progress: json['progress'] as num,
|
||||
isNotified: json['is_notified'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$UserTrophyToJson(UserTrophy instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'trophy': instance.trophy,
|
||||
'earned_at': instance.earnedAt.toIso8601String(),
|
||||
'progress': instance.progress,
|
||||
'is_notified': instance.isNotified,
|
||||
};
|
||||
@@ -19,33 +19,70 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/models/trophies/trophy.dart';
|
||||
import 'package:wger/models/trophies/user_trophy.dart';
|
||||
import 'package:wger/models/trophies/user_trophy_progression.dart';
|
||||
import 'package:wger/providers/wger_base_riverpod.dart';
|
||||
|
||||
import 'base_provider.dart';
|
||||
|
||||
part 'trophies.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<Trophy>> trophies(Ref ref) async {
|
||||
const trophiesPath = 'trophy';
|
||||
class TrophyRepository {
|
||||
final WgerBaseProvider base;
|
||||
final trophiesPath = 'trophy';
|
||||
final userTrophiesPath = 'user-trophy';
|
||||
final userTrophyProgressionPath = 'trophy/progress';
|
||||
|
||||
final baseProvider = ref.read(wgerBaseProvider);
|
||||
TrophyRepository(this.base);
|
||||
|
||||
final trophyData = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}),
|
||||
);
|
||||
Future<List<Trophy>> fetchTrophies() async {
|
||||
final url = base.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE});
|
||||
final trophyData = await base.fetchPaginated(url);
|
||||
return trophyData.map((e) => Trophy.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
return trophyData.map((e) => Trophy.fromJson(e)).toList();
|
||||
Future<List<UserTrophy>> fetchUserTrophies() async {
|
||||
final url = base.makeUrl(userTrophiesPath, query: {'limit': API_MAX_PAGE_SIZE});
|
||||
final trophyData = await base.fetchPaginated(url);
|
||||
return trophyData.map((e) => UserTrophy.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<List<UserTrophyProgression>> fetchProgression() async {
|
||||
final url = base.makeUrl(userTrophyProgressionPath, query: {'limit': API_MAX_PAGE_SIZE});
|
||||
final data = await base.fetchPaginated(url);
|
||||
return data.map((e) => UserTrophyProgression.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
List<Trophy> filterByType(List<Trophy> list, TrophyType type) =>
|
||||
list.where((t) => t.type == type).toList();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<UserTrophyProgression>> trophyProgression(Ref ref) async {
|
||||
const userTrophyProgressionPath = 'trophy/progress';
|
||||
|
||||
final baseProvider = ref.read(wgerBaseProvider);
|
||||
|
||||
final trophyData = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(userTrophyProgressionPath, query: {'limit': API_MAX_PAGE_SIZE}),
|
||||
);
|
||||
|
||||
return trophyData.map((e) => UserTrophyProgression.fromJson(e)).toList();
|
||||
TrophyRepository trophyRepository(Ref ref) {
|
||||
final base = ref.read(wgerBaseProvider);
|
||||
return TrophyRepository(base);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
final class TrophyStateNotifier extends _$TrophyStateNotifier {
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
/// Fetch all available trophies
|
||||
Future<List<Trophy>> fetchTrophies() async {
|
||||
final repo = ref.read(trophyRepositoryProvider);
|
||||
return repo.fetchTrophies();
|
||||
}
|
||||
|
||||
/// Fetch trophies awarded to the user
|
||||
Future<List<UserTrophy>> fetchUserTrophies() async {
|
||||
final repo = ref.read(trophyRepositoryProvider);
|
||||
return repo.fetchUserTrophies();
|
||||
}
|
||||
|
||||
/// Fetch trophy progression for the user
|
||||
Future<List<UserTrophyProgression>> fetchTrophyProgression() async {
|
||||
final repo = ref.read(trophyRepositoryProvider);
|
||||
return repo.fetchProgression();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,77 +27,89 @@ part of 'trophies.dart';
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(trophies)
|
||||
const trophiesProvider = TrophiesProvider._();
|
||||
@ProviderFor(trophyRepository)
|
||||
const trophyRepositoryProvider = TrophyRepositoryProvider._();
|
||||
|
||||
final class TrophiesProvider
|
||||
extends $FunctionalProvider<AsyncValue<List<Trophy>>, List<Trophy>, FutureOr<List<Trophy>>>
|
||||
with $FutureModifier<List<Trophy>>, $FutureProvider<List<Trophy>> {
|
||||
const TrophiesProvider._()
|
||||
final class TrophyRepositoryProvider
|
||||
extends $FunctionalProvider<TrophyRepository, TrophyRepository, TrophyRepository>
|
||||
with $Provider<TrophyRepository> {
|
||||
const TrophyRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'trophiesProvider',
|
||||
name: r'trophyRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$trophiesHash();
|
||||
String debugGetCreateSourceHash() => _$trophyRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<Trophy>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
$ProviderElement<TrophyRepository> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<Trophy>> create(Ref ref) {
|
||||
return trophies(ref);
|
||||
TrophyRepository create(Ref ref) {
|
||||
return trophyRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(TrophyRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<TrophyRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$trophiesHash() => r'44dd5e9a820f4e37599daac2a49a9358386758a8';
|
||||
String _$trophyRepositoryHash() => r'0699f0c0f7f324f3ba9b21420d9845a3e3096b61';
|
||||
|
||||
@ProviderFor(trophyProgression)
|
||||
const trophyProgressionProvider = TrophyProgressionProvider._();
|
||||
@ProviderFor(TrophyStateNotifier)
|
||||
const trophyStateProvider = TrophyStateNotifierProvider._();
|
||||
|
||||
final class TrophyProgressionProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<UserTrophyProgression>>,
|
||||
List<UserTrophyProgression>,
|
||||
FutureOr<List<UserTrophyProgression>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<UserTrophyProgression>>,
|
||||
$FutureProvider<List<UserTrophyProgression>> {
|
||||
const TrophyProgressionProvider._()
|
||||
final class TrophyStateNotifierProvider extends $NotifierProvider<TrophyStateNotifier, void> {
|
||||
const TrophyStateNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'trophyProgressionProvider',
|
||||
isAutoDispose: true,
|
||||
name: r'trophyStateProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$trophyProgressionHash();
|
||||
String debugGetCreateSourceHash() => _$trophyStateNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<UserTrophyProgression>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
TrophyStateNotifier create() => TrophyStateNotifier();
|
||||
|
||||
@override
|
||||
FutureOr<List<UserTrophyProgression>> create(Ref ref) {
|
||||
return trophyProgression(ref);
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$trophyProgressionHash() => r'444caf04f3d0a7845e840d452e4b4a822b59df9b';
|
||||
String _$trophyStateNotifierHash() => r'e5c8f2a9477b8f7e5efe4e9ba23765f951627a9f';
|
||||
|
||||
abstract class _$TrophyStateNotifier extends $Notifier<void> {
|
||||
void build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
build();
|
||||
final ref = this.ref as $Ref<void, void>;
|
||||
final element =
|
||||
ref.element as $ClassProviderElement<AnyNotifier<void, void>, void, Object?, Object?>;
|
||||
element.handleValue(ref, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -26,6 +26,8 @@ import 'package:wger/widgets/dashboard/widgets/nutrition.dart';
|
||||
import 'package:wger/widgets/dashboard/widgets/routines.dart';
|
||||
import 'package:wger/widgets/dashboard/widgets/weight.dart';
|
||||
|
||||
import '../widgets/dashboard/widgets/trophies.dart';
|
||||
|
||||
class DashboardScreen extends StatelessWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@@ -48,6 +50,7 @@ class DashboardScreen extends StatelessWidget {
|
||||
}
|
||||
|
||||
final items = [
|
||||
const DashboardTrophiesWidget(),
|
||||
const DashboardRoutineWidget(),
|
||||
const DashboardNutritionWidget(),
|
||||
const DashboardWeightWidget(),
|
||||
|
||||
65
lib/widgets/dashboard/widgets/trophies.dart
Normal file
65
lib/widgets/dashboard/widgets/trophies.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wger/providers/trophies.dart';
|
||||
import 'package:wger/widgets/core/progress_indicator.dart';
|
||||
|
||||
class DashboardTrophiesWidget extends ConsumerWidget {
|
||||
const DashboardTrophiesWidget();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final provider = ref.watch(trophyStateProvider.notifier);
|
||||
|
||||
return FutureBuilder(
|
||||
future: provider.fetchUserTrophies(),
|
||||
builder: (context, asyncSnapshot) {
|
||||
if (asyncSnapshot.connectionState != ConnectionState.done) {
|
||||
return const Card(
|
||||
child: BoxedProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Trophies',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
|
||||
...(asyncSnapshot.data ?? []).map(
|
||||
(userTrophy) => ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: NetworkImage(userTrophy.trophy.image),
|
||||
),
|
||||
title: Text(userTrophy.trophy.name),
|
||||
subtitle: Text(userTrophy.trophy.description, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,11 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:wger/providers/base_provider.dart';
|
||||
import 'package:wger/providers/trophies.dart';
|
||||
import 'package:wger/providers/wger_base_riverpod.dart';
|
||||
|
||||
import 'trophies_provider_test.mocks.dart';
|
||||
|
||||
@@ -39,8 +37,8 @@ const trophyJson = {
|
||||
|
||||
@GenerateMocks([WgerBaseProvider])
|
||||
void main() {
|
||||
group('Trophies providers', () {
|
||||
test('trophies provider returns list of Trophy models', () async {
|
||||
group('Trophy repository', () {
|
||||
test('fetches list of trophies', () async {
|
||||
// Arrange
|
||||
final mockBase = MockWgerBaseProvider();
|
||||
when(mockBase.fetchPaginated(any)).thenAnswer((_) async => [trophyJson]);
|
||||
@@ -52,15 +50,10 @@ void main() {
|
||||
query: anyNamed('query'),
|
||||
),
|
||||
).thenReturn(Uri.parse('https://example.org/trophies'));
|
||||
|
||||
final container = ProviderContainer.test(
|
||||
overrides: [
|
||||
wgerBaseProvider.overrideWithValue(mockBase),
|
||||
],
|
||||
);
|
||||
final repository = TrophyRepository(mockBase);
|
||||
|
||||
// Act
|
||||
final result = await container.read(trophiesProvider.future);
|
||||
final result = await repository.fetchTrophies();
|
||||
|
||||
// Assert
|
||||
expect(result, isA<List>());
|
||||
@@ -71,7 +64,7 @@ void main() {
|
||||
expect(trophy.type.toString(), contains('count'));
|
||||
});
|
||||
|
||||
test('trophyProgression provider returns list of UserTrophyProgression models', () async {
|
||||
test('fetches list of user trophy progression', () async {
|
||||
// Arrange
|
||||
final progressionJson = {
|
||||
'trophy': trophyJson,
|
||||
@@ -93,14 +86,10 @@ void main() {
|
||||
query: anyNamed('query'),
|
||||
),
|
||||
).thenReturn(Uri.parse('https://example.org/user_progressions'));
|
||||
final container = ProviderContainer.test(
|
||||
overrides: [
|
||||
wgerBaseProvider.overrideWithValue(mockBase),
|
||||
],
|
||||
);
|
||||
final repository = TrophyRepository(mockBase);
|
||||
|
||||
// Act
|
||||
final result = await container.read(trophyProgressionProvider.future);
|
||||
final result = await repository.fetchProgression();
|
||||
|
||||
// Assert
|
||||
expect(result, isA<List>());
|
||||
|
||||
Reference in New Issue
Block a user