refactor: rebuild using drift instead of isar
This commit is contained in:
parent
952e82eb08
commit
ee89f327bd
25 changed files with 2182 additions and 6389 deletions
|
|
@ -1,13 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
|
|
||||||
import 'src/app.dart';
|
import 'src/app.dart';
|
||||||
import 'src/shared/data/local/collections/user_collection.dart';
|
import 'src/shared/data/local/app_database.dart';
|
||||||
import 'src/shared/data/local/collections/cycle_collection.dart';
|
|
||||||
import 'src/shared/data/local/collections/workout_collection.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
@ -17,19 +12,15 @@ void main() async {
|
||||||
DeviceOrientation.portraitDown,
|
DeviceOrientation.portraitDown,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
final database = AppDatabase();
|
||||||
final isar = await Isar.open(
|
|
||||||
[UserCollectionSchema, CycleCollectionSchema, WorkoutCollectionSchema],
|
|
||||||
directory: dir.path,
|
|
||||||
name: 'slrpg_db',
|
|
||||||
);
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [isarProvider.overrideWithValue(isar)],
|
overrides: [appDatabaseProvider.overrideWithValue(database)],
|
||||||
child: const SLRPGApp(),
|
child: const SLRPGApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final isarProvider = Provider<Isar>((ref) => throw UnimplementedError());
|
final appDatabaseProvider =
|
||||||
|
Provider<AppDatabase>((ref) => throw UnimplementedError());
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,22 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
String message = 'Login failed. Please try again.';
|
||||||
|
final errorStr = e.toString();
|
||||||
|
|
||||||
|
if (errorStr.contains('400')) {
|
||||||
|
message = 'Invalid email or password.';
|
||||||
|
} else if (errorStr.contains('SocketException') ||
|
||||||
|
errorStr.contains('Connection refused') ||
|
||||||
|
errorStr.contains('Network is unreachable')) {
|
||||||
|
message = 'Could not connect to server. Please check your internet.';
|
||||||
|
}
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Login failed: ${e.toString()}'),
|
content: Text(message),
|
||||||
backgroundColor: AppTheme.errorColor,
|
backgroundColor: AppTheme.errorColor,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +82,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Logo
|
|
||||||
Container(
|
Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
|
|
@ -85,7 +96,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
'WELCOME BACK',
|
'WELCOME BACK',
|
||||||
style: Theme.of(context).textTheme.displayMedium,
|
style: Theme.of(context).textTheme.displayMedium,
|
||||||
|
|
@ -98,7 +108,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
|
@ -117,7 +126,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: _obscurePassword,
|
obscureText: _obscurePassword,
|
||||||
|
|
@ -146,7 +154,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isLoading ? null : _handleLogin,
|
onPressed: _isLoading ? null : _handleLogin,
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
|
|
@ -161,7 +168,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||||
: const Text('LOGIN'),
|
: const Text('LOGIN'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
|
import 'package:drift/drift.dart' hide Column;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
import '../../../../shared/data/local/collections/user_collection.dart';
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
import '../../../gamification/domain/entities/avatar_config.dart';
|
import '../../../gamification/domain/entities/avatar_config.dart';
|
||||||
import '../../../gamification/presentation/widgets/avatar_editor.dart';
|
import '../../../gamification/presentation/widgets/avatar_editor.dart';
|
||||||
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class ProfileScreen extends ConsumerStatefulWidget {
|
class ProfileScreen extends ConsumerStatefulWidget {
|
||||||
const ProfileScreen({super.key});
|
const ProfileScreen({super.key});
|
||||||
|
|
@ -46,6 +46,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
await ref
|
await ref
|
||||||
.read(userRepositoryProvider)
|
.read(userRepositoryProvider)
|
||||||
.updateBodyweight(_currentBodyweight);
|
.updateBodyweight(_currentBodyweight);
|
||||||
|
|
||||||
|
if (_user != null) {
|
||||||
|
_user = _user!.copyWith(currentBodyweight: _currentBodyweight);
|
||||||
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _hasChanges = false);
|
setState(() => _hasChanges = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -163,8 +168,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showAvatarEditor() {
|
void _showAvatarEditor() {
|
||||||
final currentConfig = _user?.avatarConfigJson != null
|
final currentConfig = _user?.avatarConfig != null
|
||||||
? AvatarConfig.fromJson(jsonDecode(_user!.avatarConfigJson!))
|
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
||||||
: const AvatarConfig();
|
: const AvatarConfig();
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
|
|
@ -199,8 +204,13 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
).then((result) async {
|
).then((result) async {
|
||||||
if (result is AvatarConfig) {
|
if (result is AvatarConfig) {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
_user!.avatarConfigJson = jsonEncode(result.toJson());
|
|
||||||
_user!.isDirty = true;
|
final updatedUser = _user!.copyWith(
|
||||||
|
avatarConfig: Value(result.toJson()),
|
||||||
|
isDirty: true,
|
||||||
|
);
|
||||||
|
_user = updatedUser;
|
||||||
|
|
||||||
await ref.read(userRepositoryProvider).saveLocalUser(_user!);
|
await ref.read(userRepositoryProvider).saveLocalUser(_user!);
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
|
|
@ -210,8 +220,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final userRepo = ref.watch(userRepositoryProvider);
|
final userRepo = ref.watch(userRepositoryProvider);
|
||||||
final avatarConfig = _user?.avatarConfigJson != null
|
final avatarConfig = _user?.avatarConfig != null
|
||||||
? AvatarConfig.fromJson(jsonDecode(_user!.avatarConfigJson!))
|
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
||||||
: const AvatarConfig();
|
: const AvatarConfig();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,20 @@ import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../../core/constants/asset_paths.dart';
|
import '../../../../core/constants/asset_paths.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
import '../../../../shared/data/repositories/cycle_repository.dart';
|
import '../../../../shared/data/repositories/cycle_repository.dart';
|
||||||
import '../../../../shared/domain/logic/xp_calculator.dart';
|
|
||||||
import '../widgets/xp_bar_widget.dart';
|
|
||||||
import '../widgets/level_display.dart';
|
|
||||||
import '../widgets/start_raid_button.dart';
|
|
||||||
import '../../../../shared/data/local/collections/user_collection.dart';
|
|
||||||
import '../../../../shared/data/local/collections/cycle_collection.dart';
|
|
||||||
import '../../../../shared/data/repositories/workout_repository.dart';
|
import '../../../../shared/data/repositories/workout_repository.dart';
|
||||||
import '../../../../shared/domain/entities/exercise.dart';
|
|
||||||
import '../../../../shared/domain/logic/wendler_calculator.dart';
|
|
||||||
import '../../../../shared/data/remote/sync_service.dart';
|
import '../../../../shared/data/remote/sync_service.dart';
|
||||||
|
import '../../../../shared/domain/logic/xp_calculator.dart';
|
||||||
|
import '../../../../shared/domain/logic/wendler_calculator.dart';
|
||||||
|
import '../../../../shared/domain/entities/exercise.dart';
|
||||||
import '../../../../shared/domain/entities/workout_set.dart';
|
import '../../../../shared/domain/entities/workout_set.dart';
|
||||||
import '../../../gamification/domain/entities/avatar_config.dart';
|
import '../../../gamification/domain/entities/avatar_config.dart';
|
||||||
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
||||||
|
import '../widgets/xp_bar_widget.dart';
|
||||||
|
import '../widgets/level_display.dart';
|
||||||
|
import '../widgets/start_raid_button.dart';
|
||||||
|
|
||||||
class HubScreen extends ConsumerStatefulWidget {
|
class HubScreen extends ConsumerStatefulWidget {
|
||||||
const HubScreen({super.key});
|
const HubScreen({super.key});
|
||||||
|
|
@ -41,7 +40,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_runSync();
|
_runSync();
|
||||||
});
|
});
|
||||||
|
|
@ -104,7 +102,10 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
CycleCollection cycle, UserCollection user) async {
|
CycleCollection cycle, UserCollection user) async {
|
||||||
try {
|
try {
|
||||||
final workoutRepo = ref.read(workoutRepositoryProvider);
|
final workoutRepo = ref.read(workoutRepositoryProvider);
|
||||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
|
||||||
|
final tmsDynamic = cycle.trainingMaxes;
|
||||||
|
final trainingMaxes = Map<String, double>.from(tmsDynamic
|
||||||
|
.map((key, value) => MapEntry(key, (value as num).toDouble())));
|
||||||
|
|
||||||
int targetWeek = 1;
|
int targetWeek = 1;
|
||||||
int targetDay = 1;
|
int targetDay = 1;
|
||||||
|
|
@ -138,8 +139,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final trainingMaxes = cycleRepo.getCurrentTrainingMaxes();
|
|
||||||
|
|
||||||
var workout = await workoutRepo.getWorkoutByWeekDay(
|
var workout = await workoutRepo.getWorkoutByWeekDay(
|
||||||
cycleId: cycleRefId,
|
cycleId: cycleRefId,
|
||||||
localCycleId: localCycleId,
|
localCycleId: localCycleId,
|
||||||
|
|
@ -162,7 +161,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
cycleId: cycleRefId,
|
cycleId: cycleRefId,
|
||||||
week: targetWeek,
|
week: targetWeek,
|
||||||
day: targetDay,
|
day: targetDay,
|
||||||
exercisesJson: jsonEncode(exercises.map((e) => e.toJson()).toList()),
|
exercises: exercises.map((e) => e.toJson()).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,8 +201,9 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
|
|
||||||
final user = snapshot.data![0] as UserCollection?;
|
final user = snapshot.data![0] as UserCollection?;
|
||||||
final cycle = snapshot.data![1] as CycleCollection?;
|
final cycle = snapshot.data![1] as CycleCollection?;
|
||||||
final avatarConfig = user?.avatarConfigJson != null
|
|
||||||
? AvatarConfig.fromJson(jsonDecode(user!.avatarConfigJson!))
|
final avatarConfig = user?.avatarConfig != null
|
||||||
|
? AvatarConfig.fromJson(user!.avatarConfig!)
|
||||||
: const AvatarConfig();
|
: const AvatarConfig();
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
import '../../../../shared/data/repositories/workout_repository.dart';
|
import '../../../../shared/data/repositories/workout_repository.dart';
|
||||||
import '../../../../shared/data/local/collections/workout_collection.dart';
|
|
||||||
import '../../../../shared/domain/entities/exercise.dart';
|
import '../../../../shared/domain/entities/exercise.dart';
|
||||||
import '../../../../shared/domain/entities/workout_set.dart';
|
|
||||||
|
|
||||||
class HistoryScreen extends ConsumerStatefulWidget {
|
class HistoryScreen extends ConsumerStatefulWidget {
|
||||||
const HistoryScreen({super.key});
|
const HistoryScreen({super.key});
|
||||||
|
|
@ -27,13 +25,11 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
if (user == null) return [];
|
if (user == null) return [];
|
||||||
|
|
||||||
final userId = user.serverId ?? user.id.toString();
|
final userId = user.serverId ?? user.id.toString();
|
||||||
return workoutRepo.getCompletedWorkouts(userId); // ID übergeben
|
return workoutRepo.getCompletedWorkouts(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final workoutRepo = ref.watch(workoutRepositoryProvider);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Quest Log'),
|
title: const Text('Quest Log'),
|
||||||
|
|
@ -74,7 +70,7 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final workouts = snapshot.data!
|
final workouts = List<WorkoutCollection>.from(snapshot.data!)
|
||||||
..sort((a, b) => b.completedAt!.compareTo(a.completedAt!));
|
..sort((a, b) => b.completedAt!.compareTo(a.completedAt!));
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
|
@ -98,8 +94,10 @@ class _WorkoutHistoryCard extends StatelessWidget {
|
||||||
|
|
||||||
List<Exercise> _parseExercises() {
|
List<Exercise> _parseExercises() {
|
||||||
try {
|
try {
|
||||||
final List<dynamic> jsonList = jsonDecode(workout.exercisesJson);
|
final List<dynamic> list = workout.exercises;
|
||||||
return jsonList.map((json) => Exercise.fromJson(json)).toList();
|
return list
|
||||||
|
.map((json) => Exercise.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error parsing workout history: $e');
|
debugPrint('Error parsing workout history: $e');
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -237,10 +235,10 @@ class _ExerciseDetailRow extends StatelessWidget {
|
||||||
Table(
|
Table(
|
||||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
columnWidths: const {
|
columnWidths: const {
|
||||||
0: FlexColumnWidth(1), // Set
|
0: FlexColumnWidth(1),
|
||||||
1: FlexColumnWidth(2), // Weight
|
1: FlexColumnWidth(2),
|
||||||
2: FlexColumnWidth(2), // Reps
|
2: FlexColumnWidth(2),
|
||||||
3: FlexColumnWidth(1), // Type (AMRAP/FSL)
|
3: FlexColumnWidth(1),
|
||||||
},
|
},
|
||||||
children: exercise.sets.where((s) => s.completed).map((set) {
|
children: exercise.sets.where((s) => s.completed).map((set) {
|
||||||
return TableRow(
|
return TableRow(
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
||||||
|
|
||||||
Future<void> _loadCurrentInventory() async {
|
Future<void> _loadCurrentInventory() async {
|
||||||
final userRepo = ref.read(userRepositoryProvider);
|
final userRepo = ref.read(userRepositoryProvider);
|
||||||
final inventory = userRepo.getInventorySettings();
|
final inventory = await userRepo.getInventorySettingsAsync();
|
||||||
|
|
||||||
final barWeight = (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
final barWeight = (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
||||||
|
|
||||||
|
|
@ -62,7 +62,8 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var b in bandsList) {
|
for (var b in bandsList) {
|
||||||
final color = b['color'] as String;
|
final band = b as Map<String, dynamic>;
|
||||||
|
final color = band['color'] as String;
|
||||||
if (bandMap.containsKey(color)) {
|
if (bandMap.containsKey(color)) {
|
||||||
bandMap[color] = true;
|
bandMap[color] = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:drift/drift.dart' hide Column;
|
||||||
|
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../core/constants/app_constants.dart';
|
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
import '../../../../shared/data/repositories/cycle_repository.dart';
|
import '../../../../shared/data/repositories/cycle_repository.dart';
|
||||||
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
import '../../../gamification/domain/entities/avatar_config.dart';
|
import '../../../gamification/domain/entities/avatar_config.dart';
|
||||||
import '../../../gamification/presentation/widgets/avatar_editor.dart';
|
import '../../../gamification/presentation/widgets/avatar_editor.dart';
|
||||||
import 'bodyweight_input_screen.dart';
|
import 'bodyweight_input_screen.dart';
|
||||||
|
|
@ -41,10 +41,12 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
||||||
inventorySettings: inventorySettings,
|
inventorySettings: inventorySettings,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
user.currentBodyweight =
|
user = user.copyWith(
|
||||||
onboardingData['bodyweight'] ?? user.currentBodyweight;
|
currentBodyweight:
|
||||||
user.inventorySettingsJson = jsonEncode(inventorySettings);
|
onboardingData['bodyweight'] ?? user.currentBodyweight,
|
||||||
user.isDirty = true;
|
inventorySettings: Value(inventorySettings),
|
||||||
|
isDirty: true,
|
||||||
|
);
|
||||||
await userRepo.saveLocalUser(user);
|
await userRepo.saveLocalUser(user);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -55,8 +57,10 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user!.avatarConfigJson = jsonEncode(_config.toJson());
|
user = user.copyWith(
|
||||||
user.isDirty = true;
|
avatarConfig: Value(_config.toJson()),
|
||||||
|
isDirty: true,
|
||||||
|
);
|
||||||
await userRepo.saveLocalUser(user);
|
await userRepo.saveLocalUser(user);
|
||||||
|
|
||||||
final trainingMaxes =
|
final trainingMaxes =
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../../shared/data/local/app_database.dart'; // Drift Models
|
||||||
import '../../../../shared/data/repositories/cycle_repository.dart';
|
import '../../../../shared/data/repositories/cycle_repository.dart';
|
||||||
import '../../../../shared/data/local/collections/cycle_collection.dart';
|
|
||||||
import '../../../../shared/data/remote/api_client.dart'; // Zugriff auf API
|
|
||||||
import '../../../../shared/data/repositories/user_repository.dart';
|
import '../../../../shared/data/repositories/user_repository.dart';
|
||||||
import '../../../../shared/data/repositories/workout_repository.dart';
|
import '../../../../shared/data/repositories/workout_repository.dart';
|
||||||
import '../../../../shared/domain/entities/exercise.dart';
|
import '../../../../shared/domain/entities/exercise.dart';
|
||||||
|
|
@ -55,19 +53,15 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||||||
for (var workout in allWorkouts) {
|
for (var workout in allWorkouts) {
|
||||||
if (workout.completedAt == null) continue;
|
if (workout.completedAt == null) continue;
|
||||||
|
|
||||||
List<dynamic> exercisesJson = [];
|
final exercisesList = workout.exercises;
|
||||||
try {
|
|
||||||
exercisesJson = jsonDecode(workout.exercisesJson);
|
|
||||||
} catch (e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
double max1RM = 0.0;
|
double max1RM = 0.0;
|
||||||
double sessionVolume = 0.0;
|
double sessionVolume = 0.0;
|
||||||
bool foundExercise = false;
|
bool foundExercise = false;
|
||||||
double trainingMax = 0.0;
|
double trainingMax = 0.0;
|
||||||
|
|
||||||
for (var exJson in exercisesJson) {
|
for (var exDynamic in exercisesList) {
|
||||||
|
final exJson = exDynamic as Map<String, dynamic>;
|
||||||
final exercise = Exercise.fromJson(exJson);
|
final exercise = Exercise.fromJson(exJson);
|
||||||
|
|
||||||
if (exercise.exerciseId == _selectedExercise) {
|
if (exercise.exerciseId == _selectedExercise) {
|
||||||
|
|
@ -147,12 +141,10 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||||||
try {
|
try {
|
||||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||||
|
|
||||||
final oldTMs =
|
final oldTMs = currentCycle.trainingMaxes;
|
||||||
jsonDecode(currentCycle.trainingMaxesJson) as Map<String, dynamic>;
|
|
||||||
|
|
||||||
final newCycle = await cycleRepo.finishCycle();
|
final newCycle = await cycleRepo.finishCycle();
|
||||||
final newTMs =
|
final newTMs = newCycle.trainingMaxes;
|
||||||
jsonDecode(newCycle.trainingMaxesJson) as Map<String, dynamic>;
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
|
|
@ -215,7 +207,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
'Progress Analysis',
|
'Progress Analysis',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
|
|
@ -224,7 +215,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||||||
?.copyWith(color: AppTheme.textPrimary),
|
?.copyWith(color: AppTheme.textPrimary),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -250,14 +240,11 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
_isChartLoading
|
_isChartLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 250,
|
height: 250,
|
||||||
child: Center(child: CircularProgressIndicator()))
|
child: Center(child: CircularProgressIndicator()))
|
||||||
: ProgressChart(data: _chartData),
|
: ProgressChart(data: _chartData),
|
||||||
|
|
||||||
// (Optional: Range Selector unten drunter '1M', '3M', '1Y'...)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -275,7 +262,8 @@ class _CurrentCycleCard extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tms = jsonDecode(cycle.trainingMaxesJson) as Map<String, dynamic>;
|
// Drift: Direct access
|
||||||
|
final tms = cycle.trainingMaxes;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
@ -380,7 +368,7 @@ class _CycleFinishDialog extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'You have defeated the guardians of this cycle. But deeper in the dungeon, stronger foes await...'), // Story
|
'You have defeated the guardians of this cycle. But deeper in the dungeon, stronger foes await...'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
@ -388,12 +376,17 @@ class _CycleFinishDialog extends StatelessWidget {
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_DiffRow(
|
_DiffRow(
|
||||||
name: 'Squat', oldVal: oldTMs['squat'], newVal: newTMs['squat']),
|
name: 'Squat',
|
||||||
|
oldVal: (oldTMs['squat'] as num).toDouble(),
|
||||||
|
newVal: (newTMs['squat'] as num).toDouble()),
|
||||||
_DiffRow(
|
_DiffRow(
|
||||||
name: 'Pull-up',
|
name: 'Pull-up',
|
||||||
oldVal: oldTMs['pullup'],
|
oldVal: (oldTMs['pullup'] as num).toDouble(),
|
||||||
newVal: newTMs['pullup']),
|
newVal: (newTMs['pullup'] as num).toDouble()),
|
||||||
_DiffRow(name: 'Dip', oldVal: oldTMs['dip'], newVal: newTMs['dip']),
|
_DiffRow(
|
||||||
|
name: 'Dip',
|
||||||
|
oldVal: (oldTMs['dip'] as num).toDouble(),
|
||||||
|
newVal: (newTMs['dip'] as num).toDouble()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import '../../../../core/constants/asset_paths.dart';
|
import '../../../../core/constants/asset_paths.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
|
@ -127,16 +126,14 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||||
|
|
||||||
final user = await userRepo.getLocalUser();
|
final user = await userRepo.getLocalUser();
|
||||||
final cycle = await cycleRepo.getCurrentCycle();
|
final trainingMaxesMap = await cycleRepo.getCurrentTrainingMaxesAsync();
|
||||||
|
|
||||||
if (user == null || cycle == null) {
|
if (user == null) {
|
||||||
if (mounted) context.go('/hub');
|
if (mounted) context.go('/hub');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final trainingMaxes = cycleRepo.getCurrentTrainingMaxes();
|
|
||||||
final exercises = <Exercise>[];
|
final exercises = <Exercise>[];
|
||||||
|
|
||||||
final exerciseConfigs = _getExerciseConfig(widget.day);
|
final exerciseConfigs = _getExerciseConfig(widget.day);
|
||||||
|
|
||||||
for (final config in exerciseConfigs) {
|
for (final config in exerciseConfigs) {
|
||||||
|
|
@ -145,7 +142,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
final type = config['type'] as ExerciseType;
|
final type = config['type'] as ExerciseType;
|
||||||
final isMain = config['isMain'] as bool;
|
final isMain = config['isMain'] as bool;
|
||||||
|
|
||||||
final tm = trainingMaxes[id] ?? 0.0;
|
final tm = trainingMaxesMap[id] ?? 0.0;
|
||||||
List<WorkoutSet> sets = [];
|
List<WorkoutSet> sets = [];
|
||||||
|
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
|
|
@ -175,6 +172,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_exercises = exercises;
|
_exercises = exercises;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
|
|
@ -184,6 +182,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _completeSet() {
|
void _completeSet() {
|
||||||
final currentExercise = _exercises[_currentExerciseIndex];
|
final currentExercise = _exercises[_currentExerciseIndex];
|
||||||
|
|
@ -286,9 +285,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
cycleId: cycleIdRef, week: widget.week, day: widget.day);
|
cycleId: cycleIdRef, week: widget.week, day: widget.day);
|
||||||
|
|
||||||
if (workout != null) {
|
if (workout != null) {
|
||||||
workout.exercisesJson =
|
final updatedExercises = _exercises.map((e) => e.toJson()).toList();
|
||||||
jsonEncode(_exercises.map((e) => e.toJson()).toList());
|
final updatedWorkout = workout.copyWith(exercises: updatedExercises);
|
||||||
await workoutRepo.completeWorkout(workout, xpEarned: xpEarned);
|
|
||||||
|
await workoutRepo.completeWorkout(updatedWorkout, xpEarned: xpEarned);
|
||||||
|
|
||||||
ref.read(syncServiceProvider).sync();
|
ref.read(syncServiceProvider).sync();
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +355,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'The monsters tremble at your new power.', // Story Flavor
|
'The monsters tremble at your new power.',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Colors.black54, fontStyle: FontStyle.italic),
|
color: Colors.black54, fontStyle: FontStyle.italic),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
@ -381,6 +381,17 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleCompletePress(WorkoutSet currentSet) {
|
||||||
|
if (currentSet.isAmrap) {
|
||||||
|
_showAmrapDialog(currentSet);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_repsCompleted = currentSet.repsTarget;
|
||||||
|
});
|
||||||
|
_completeSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
|
|
@ -396,17 +407,27 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
|
|
||||||
final currentExercise = _exercises[_currentExerciseIndex];
|
final currentExercise = _exercises[_currentExerciseIndex];
|
||||||
final currentSet = currentExercise.sets[_currentSetIndex];
|
final currentSet = currentExercise.sets[_currentSetIndex];
|
||||||
final userRepo = ref.watch(userRepositoryProvider);
|
|
||||||
|
return FutureBuilder<Map<String, dynamic>>(
|
||||||
|
future: ref.read(userRepositoryProvider).getInventorySettingsAsync(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData)
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(child: CircularProgressIndicator()));
|
||||||
|
|
||||||
|
final inventory = snapshot.data!;
|
||||||
|
|
||||||
final totalHP = _exercises.fold<int>(
|
final totalHP = _exercises.fold<int>(
|
||||||
0,
|
0,
|
||||||
(sum, ex) => sum + ex.sets.fold<int>(0, (s, set) => s + set.repsTarget),
|
(sum, ex) =>
|
||||||
|
sum + ex.sets.fold<int>(0, (s, set) => s + set.repsTarget),
|
||||||
);
|
);
|
||||||
|
|
||||||
final completedHP = _exercises.take(_currentExerciseIndex).fold<int>(
|
final completedHP = _exercises.take(_currentExerciseIndex).fold<int>(
|
||||||
0,
|
0,
|
||||||
(sum, ex) =>
|
(sum, ex) =>
|
||||||
sum + ex.sets.fold<int>(0, (s, set) => s + set.repsActual),
|
sum +
|
||||||
|
ex.sets.fold<int>(0, (s, set) => s + set.repsActual),
|
||||||
) +
|
) +
|
||||||
currentExercise.sets
|
currentExercise.sets
|
||||||
.take(_currentSetIndex)
|
.take(_currentSetIndex)
|
||||||
|
|
@ -415,12 +436,15 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
final isBodyweight = currentExercise.exerciseId != 'squat';
|
final isBodyweight = currentExercise.exerciseId != 'squat';
|
||||||
final barWeight = isBodyweight
|
final barWeight = isBodyweight
|
||||||
? currentExercise.bodyweightAtSession
|
? currentExercise.bodyweightAtSession
|
||||||
: userRepo.getBarWeight();
|
: (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
||||||
final availablePlates = userRepo.getAvailablePlates();
|
|
||||||
final inventory = userRepo.getInventorySettings();
|
final platesList = (inventory['plates'] as List?)
|
||||||
|
?.map((e) => (e as num).toDouble())
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
final bandsList =
|
final bandsList =
|
||||||
(inventory['bands'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
(inventory['bands'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||||
|
|
||||||
final Map<String, double> availableBands = {};
|
final Map<String, double> availableBands = {};
|
||||||
for (var band in bandsList) {
|
for (var band in bandsList) {
|
||||||
final color = band['color'] as String;
|
final color = band['color'] as String;
|
||||||
|
|
@ -433,7 +457,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
final plateResult = PlateCalculator.calculate(
|
final plateResult = PlateCalculator.calculate(
|
||||||
targetWeight: currentSet.targetWeightTotal,
|
targetWeight: currentSet.targetWeightTotal,
|
||||||
barWeight: barWeight,
|
barWeight: barWeight,
|
||||||
availablePlates: availablePlates,
|
availablePlates: platesList,
|
||||||
availableBands: availableBands,
|
availableBands: availableBands,
|
||||||
isTwoSided: !isBodyweight,
|
isTwoSided: !isBodyweight,
|
||||||
);
|
);
|
||||||
|
|
@ -485,12 +509,13 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: _isResting
|
child: _isResting
|
||||||
? _buildRestScreen()
|
? _buildRestScreen()
|
||||||
: _buildWorkoutScreen(currentExercise, currentSet, plateResult,
|
: _buildWorkoutScreen(currentExercise, currentSet,
|
||||||
completedHP, totalHP),
|
plateResult, completedHP, totalHP),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRestScreen() {
|
Widget _buildRestScreen() {
|
||||||
|
|
@ -759,17 +784,6 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleCompletePress(WorkoutSet currentSet) {
|
|
||||||
if (currentSet.isAmrap) {
|
|
||||||
_showAmrapDialog(currentSet);
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_repsCompleted = currentSet.repsTarget;
|
|
||||||
});
|
|
||||||
_completeSet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatTime(int seconds) {
|
String _formatTime(int seconds) {
|
||||||
final minutes = seconds ~/ 60;
|
final minutes = seconds ~/ 60;
|
||||||
final secs = seconds % 60;
|
final secs = seconds % 60;
|
||||||
|
|
|
||||||
32
lib/src/shared/data/local/app_database.dart
Normal file
32
lib/src/shared/data/local/app_database.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'tables.dart';
|
||||||
|
import 'converters/json_converter.dart';
|
||||||
|
|
||||||
|
part 'app_database.g.dart';
|
||||||
|
|
||||||
|
@DriftDatabase(tables: [Users, Cycles, Workouts])
|
||||||
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
AppDatabase() : super(_openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
onCreate: (Migrator m) async {
|
||||||
|
await m.createAll();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyDatabase _openConnection() {
|
||||||
|
return LazyDatabase(() async {
|
||||||
|
final dbFolder = await getApplicationDocumentsDirectory();
|
||||||
|
final file = File(p.join(dbFolder.path, 'slrpg.sqlite'));
|
||||||
|
return NativeDatabase.createInBackground(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
1345
lib/src/shared/data/local/app_database.g.dart
Normal file
1345
lib/src/shared/data/local/app_database.g.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,27 +0,0 @@
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
|
|
||||||
part 'cycle_collection.g.dart';
|
|
||||||
|
|
||||||
@collection
|
|
||||||
class CycleCollection {
|
|
||||||
Id id = Isar.autoIncrement;
|
|
||||||
|
|
||||||
@Index(unique: true)
|
|
||||||
String? serverId;
|
|
||||||
|
|
||||||
String userId = ''; // Local reference
|
|
||||||
int cycleNumber = 1;
|
|
||||||
|
|
||||||
DateTime startDate = DateTime.now();
|
|
||||||
DateTime? endDate;
|
|
||||||
|
|
||||||
bool isActive = true;
|
|
||||||
|
|
||||||
// Training Maxes (stored as JSON string)
|
|
||||||
String trainingMaxesJson = '{}';
|
|
||||||
|
|
||||||
bool isDirty = false;
|
|
||||||
DateTime createdAt = DateTime.now();
|
|
||||||
DateTime updatedAt = DateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +0,0 @@
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
|
|
||||||
part 'user_collection.g.dart';
|
|
||||||
|
|
||||||
@collection
|
|
||||||
class UserCollection {
|
|
||||||
Id id = Isar.autoIncrement;
|
|
||||||
|
|
||||||
@Index(unique: true)
|
|
||||||
String? serverId;
|
|
||||||
|
|
||||||
String email = '';
|
|
||||||
int xp = 0;
|
|
||||||
int level = 1;
|
|
||||||
double currentBodyweight = 70.0;
|
|
||||||
|
|
||||||
String? inventorySettingsJson;
|
|
||||||
String? avatarConfigJson;
|
|
||||||
|
|
||||||
DateTime? lastSyncAt;
|
|
||||||
bool isDirty = false;
|
|
||||||
|
|
||||||
DateTime createdAt = DateTime.now();
|
|
||||||
DateTime updatedAt = DateTime.now();
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,32 +0,0 @@
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
|
|
||||||
part 'workout_collection.g.dart';
|
|
||||||
|
|
||||||
@collection
|
|
||||||
class WorkoutCollection {
|
|
||||||
Id id = Isar.autoIncrement;
|
|
||||||
|
|
||||||
// @Index(unique: true)
|
|
||||||
@Index()
|
|
||||||
String? serverId;
|
|
||||||
|
|
||||||
String userId = '';
|
|
||||||
String cycleId = '';
|
|
||||||
|
|
||||||
int week = 1; // 1-4
|
|
||||||
int day = 1; // 1-3
|
|
||||||
|
|
||||||
DateTime? scheduledDate;
|
|
||||||
DateTime? completedAt;
|
|
||||||
|
|
||||||
int xpEarned = 0;
|
|
||||||
|
|
||||||
// Exercises data (JSON string)
|
|
||||||
String exercisesJson = '[]';
|
|
||||||
|
|
||||||
String notes = '';
|
|
||||||
|
|
||||||
bool isDirty = false;
|
|
||||||
DateTime createdAt = DateTime.now();
|
|
||||||
DateTime updatedAt = DateTime.now();
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
36
lib/src/shared/data/local/converters/json_converter.dart
Normal file
36
lib/src/shared/data/local/converters/json_converter.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
class MapConverter extends TypeConverter<Map<String, dynamic>, String> {
|
||||||
|
const MapConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> fromSql(String fromDb) {
|
||||||
|
if (fromDb.isEmpty) return {};
|
||||||
|
try {
|
||||||
|
return json.decode(fromDb) as Map<String, dynamic>;
|
||||||
|
} catch (_) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(Map<String, dynamic> value) => json.encode(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListConverter extends TypeConverter<List<dynamic>, String> {
|
||||||
|
const ListConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<dynamic> fromSql(String fromDb) {
|
||||||
|
if (fromDb.isEmpty) return [];
|
||||||
|
try {
|
||||||
|
return json.decode(fromDb) as List<dynamic>;
|
||||||
|
} catch (_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(List<dynamic> value) => json.encode(value);
|
||||||
|
}
|
||||||
66
lib/src/shared/data/local/tables.dart
Normal file
66
lib/src/shared/data/local/tables.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'converters/json_converter.dart';
|
||||||
|
|
||||||
|
@DataClassName('UserCollection')
|
||||||
|
class Users extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get serverId => text().nullable().unique()();
|
||||||
|
|
||||||
|
TextColumn get email => text().withDefault(const Constant(''))();
|
||||||
|
IntColumn get xp => integer().withDefault(const Constant(0))();
|
||||||
|
IntColumn get level => integer().withDefault(const Constant(1))();
|
||||||
|
RealColumn get currentBodyweight =>
|
||||||
|
real().withDefault(const Constant(70.0))();
|
||||||
|
|
||||||
|
TextColumn get inventorySettings =>
|
||||||
|
text().map(const MapConverter()).nullable()();
|
||||||
|
TextColumn get avatarConfig => text().map(const MapConverter()).nullable()();
|
||||||
|
|
||||||
|
DateTimeColumn get lastSyncAt => dateTime().nullable()();
|
||||||
|
BoolColumn get isDirty => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DataClassName('CycleCollection')
|
||||||
|
class Cycles extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get serverId => text().nullable().unique()();
|
||||||
|
|
||||||
|
TextColumn get userId => text()();
|
||||||
|
|
||||||
|
IntColumn get cycleNumber => integer()();
|
||||||
|
DateTimeColumn get startDate => dateTime()();
|
||||||
|
DateTimeColumn get endDate => dateTime().nullable()();
|
||||||
|
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
|
||||||
|
|
||||||
|
TextColumn get trainingMaxes => text().map(const MapConverter())();
|
||||||
|
|
||||||
|
BoolColumn get isDirty => boolean().withDefault(const Constant(false))();
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DataClassName('WorkoutCollection')
|
||||||
|
class Workouts extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get serverId => text().nullable().unique()();
|
||||||
|
|
||||||
|
TextColumn get userId => text()();
|
||||||
|
TextColumn get cycleId => text()();
|
||||||
|
|
||||||
|
IntColumn get week => integer()();
|
||||||
|
IntColumn get day => integer()();
|
||||||
|
|
||||||
|
DateTimeColumn get scheduledDate => dateTime().nullable()();
|
||||||
|
DateTimeColumn get completedAt => dateTime().nullable()();
|
||||||
|
IntColumn get xpEarned => integer().withDefault(const Constant(0))();
|
||||||
|
|
||||||
|
TextColumn get exercises => text().map(const ListConverter())();
|
||||||
|
TextColumn get notes => text().withDefault(const Constant(''))();
|
||||||
|
|
||||||
|
BoolColumn get isDirty => boolean().withDefault(const Constant(false))();
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,28 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
import '../../../core/constants/app_constants.dart';
|
import '../../../core/constants/app_constants.dart';
|
||||||
import '../local/collections/user_collection.dart';
|
import '../local/app_database.dart';
|
||||||
import '../local/collections/cycle_collection.dart';
|
|
||||||
import '../local/collections/workout_collection.dart';
|
|
||||||
import 'api_client.dart';
|
|
||||||
import '../repositories/user_repository.dart';
|
import '../repositories/user_repository.dart';
|
||||||
|
import 'api_client.dart';
|
||||||
|
|
||||||
final syncServiceProvider = Provider<SyncService>((ref) {
|
final syncServiceProvider = Provider<SyncService>((ref) {
|
||||||
final isar = ref.watch(isarProvider);
|
final db = ref.watch(appDatabaseProvider);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
return SyncService(isar: isar, apiClient: apiClient);
|
return SyncService(db: db, apiClient: apiClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
class SyncService {
|
class SyncService {
|
||||||
final Isar isar;
|
final AppDatabase db;
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
final _storage = const FlutterSecureStorage();
|
final _storage = const FlutterSecureStorage();
|
||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
|
|
||||||
SyncService({required this.isar, required this.apiClient});
|
SyncService({required this.db, required this.apiClient});
|
||||||
|
|
||||||
Future<void> sync() async {
|
Future<void> sync() async {
|
||||||
if (_isSyncing) return;
|
if (_isSyncing) return;
|
||||||
|
|
@ -32,72 +30,66 @@ class SyncService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debugPrint('🔄 Starting Sync...');
|
debugPrint('🔄 Starting Sync...');
|
||||||
final dirtyCycles =
|
|
||||||
await isar.cycleCollections.filter().isDirtyEqualTo(true).findAll();
|
final dirtyCycles = await (db.select(db.cycles)
|
||||||
|
..where((c) => c.isDirty.equals(true)))
|
||||||
|
.get();
|
||||||
|
|
||||||
for (var cycle in dirtyCycles) {
|
for (var cycle in dirtyCycles) {
|
||||||
try {
|
try {
|
||||||
if (cycle.serverId == null) {
|
if (cycle.serverId == null) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'📤 Pushing new cycle ${cycle.cycleNumber} to server...');
|
'📤 Pushing new cycle ${cycle.cycleNumber} to server...');
|
||||||
|
final tmsMap = cycle.trainingMaxes
|
||||||
Map<String, double> tmsMap = {};
|
.map((k, v) => MapEntry(k, (v as num).toDouble()));
|
||||||
try {
|
|
||||||
final tms = jsonDecode(cycle.trainingMaxesJson);
|
|
||||||
tmsMap = Map<String, double>.from(
|
|
||||||
tms.map((k, v) => MapEntry(k, (v as num).toDouble())));
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('⚠️ Error parsing TMs for cycle ${cycle.id}: $e');
|
|
||||||
tmsMap = {'squat': 0.0, 'pullup': 0.0, 'dip': 0.0};
|
|
||||||
}
|
|
||||||
|
|
||||||
final response = await apiClient.createCycle(tmsMap);
|
final response = await apiClient.createCycle(tmsMap);
|
||||||
final newServerId = response['id'];
|
final newServerId = response['id'];
|
||||||
|
|
||||||
await isar.writeTxn(() async {
|
await db.transaction(() async {
|
||||||
cycle.serverId = newServerId;
|
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||||
cycle.isDirty = false;
|
.write(
|
||||||
await isar.cycleCollections.put(cycle);
|
CyclesCompanion(
|
||||||
|
serverId: Value(newServerId),
|
||||||
|
isDirty: const Value(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final oldLocalIdRef = cycle.id.toString();
|
final oldLocalIdRef = cycle.id.toString();
|
||||||
|
await (db.update(db.workouts)
|
||||||
final orphanWorkouts = await isar.workoutCollections
|
..where((w) => w.cycleId.equals(oldLocalIdRef)))
|
||||||
.filter()
|
.write(
|
||||||
.cycleIdEqualTo(oldLocalIdRef)
|
WorkoutsCompanion(
|
||||||
.findAll();
|
cycleId: Value(newServerId),
|
||||||
|
isDirty: const Value(true),
|
||||||
for (var w in orphanWorkouts) {
|
),
|
||||||
w.cycleId = newServerId;
|
);
|
||||||
w.isDirty = true;
|
debugPrint(
|
||||||
await isar.workoutCollections.put(w);
|
'🔗 Relinked workouts from local cycle $oldLocalIdRef to server $newServerId');
|
||||||
debugPrint('🔗 Relinked workout ${w.id} to cycle $newServerId');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await isar.writeTxn(() async {
|
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||||
cycle.isDirty = false;
|
.write(
|
||||||
await isar.cycleCollections.put(cycle);
|
const CyclesCompanion(isDirty: Value(false)),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Failed to sync cycle: $e');
|
debugPrint('❌ Failed to sync cycle ${cycle.id}: $e');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final dirtyUser =
|
final dirtyUser = await (db.select(db.users)
|
||||||
await isar.userCollections.filter().isDirtyEqualTo(true).findFirst();
|
..where((u) => u.isDirty.equals(true)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
final dirtyWorkouts = await (db.select(db.workouts)
|
||||||
|
..where((w) => w.isDirty.equals(true)))
|
||||||
|
.get();
|
||||||
|
|
||||||
final dirtyWorkouts =
|
final validWorkouts =
|
||||||
await isar.workoutCollections.filter().isDirtyEqualTo(true).findAll();
|
dirtyWorkouts.where((w) => w.cycleId.length > 5).toList();
|
||||||
|
|
||||||
if (dirtyUser == null && dirtyWorkouts.isEmpty) {
|
|
||||||
debugPrint('✅ Nothing to push.');
|
|
||||||
} else {
|
|
||||||
final pushData = <String, dynamic>{
|
final pushData = <String, dynamic>{
|
||||||
'workouts': dirtyWorkouts.where((w) {
|
'workouts': validWorkouts.map((w) {
|
||||||
return w.cycleId.length > 5;
|
|
||||||
}).map((w) {
|
|
||||||
return {
|
return {
|
||||||
'id': w.serverId,
|
'id': w.serverId,
|
||||||
'local_id': w.id,
|
'local_id': w.id,
|
||||||
|
|
@ -107,7 +99,7 @@ class SyncService {
|
||||||
'completed_at': w.completedAt?.toIso8601String(),
|
'completed_at': w.completedAt?.toIso8601String(),
|
||||||
'xp_earned': w.xpEarned,
|
'xp_earned': w.xpEarned,
|
||||||
'notes': w.notes,
|
'notes': w.notes,
|
||||||
'exercises': jsonDecode(w.exercisesJson),
|
'exercises': w.exercises,
|
||||||
};
|
};
|
||||||
}).toList(),
|
}).toList(),
|
||||||
'user_stats': dirtyUser != null
|
'user_stats': dirtyUser != null
|
||||||
|
|
@ -119,57 +111,103 @@ class SyncService {
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((pushData['workouts'] as List).length < dirtyWorkouts.length) {
|
|
||||||
debugPrint(
|
|
||||||
'⚠️ Skipped some workouts because they lack a valid server cycle ID.');
|
|
||||||
}
|
|
||||||
|
|
||||||
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
||||||
|
|
||||||
if ((pushData['workouts'] as List).isNotEmpty ||
|
debugPrint(
|
||||||
pushData['user_stats'] != null) {
|
'☁️ Contacting server (Push: ${validWorkouts.length} workouts)...');
|
||||||
debugPrint('📤 Pushing data...');
|
|
||||||
final response = await apiClient.sync(
|
final response = await apiClient.sync(
|
||||||
lastSyncTimestamp: lastSync ?? '',
|
lastSyncTimestamp: lastSync ?? '',
|
||||||
pushData: pushData,
|
pushData: pushData,
|
||||||
);
|
);
|
||||||
|
|
||||||
await isar.writeTxn(() async {
|
await db.transaction(() async {
|
||||||
if (dirtyUser != null) {
|
if (dirtyUser != null) {
|
||||||
dirtyUser.isDirty = false;
|
await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id)))
|
||||||
await isar.userCollections.put(dirtyUser);
|
.write(
|
||||||
|
const UsersCompanion(isDirty: Value(false)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (var w in validWorkouts) {
|
||||||
|
await (db.update(db.workouts)..where((dw) => dw.id.equals(w.id)))
|
||||||
|
.write(
|
||||||
|
const WorkoutsCompanion(isDirty: Value(false)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var w in dirtyWorkouts) {
|
if (response['pull_data'] != null) {
|
||||||
w.isDirty = false;
|
if (response['pull_data']['cycles'] != null) {
|
||||||
await isar.workoutCollections.put(w);
|
final pulledCycles = response['pull_data']['cycles'] as List;
|
||||||
|
for (var cJson in pulledCycles) {
|
||||||
|
final serverId = cJson['id'] as String;
|
||||||
|
final existing = await (db.select(db.cycles)
|
||||||
|
..where((c) => c.serverId.equals(serverId)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
final tms = cJson['training_maxes'] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final companion = CyclesCompanion(
|
||||||
|
serverId: Value(serverId),
|
||||||
|
userId: Value(cJson['user_id']),
|
||||||
|
cycleNumber: Value(cJson['cycle_number']),
|
||||||
|
startDate: Value(DateTime.parse(cJson['start_date'])),
|
||||||
|
endDate: Value(DateTime.tryParse(cJson['end_date'] ?? '')),
|
||||||
|
isActive: Value(cJson['is_active'] ?? false),
|
||||||
|
trainingMaxes: Value(tms),
|
||||||
|
isDirty: const Value(false),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
createdAt: existing == null
|
||||||
|
? Value(DateTime.now())
|
||||||
|
: const Value.absent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
await (db.update(db.cycles)
|
||||||
|
..where((c) => c.id.equals(existing.id)))
|
||||||
|
.write(companion);
|
||||||
|
} else {
|
||||||
|
await db.into(db.cycles).insert(companion);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response['pull_data'] != null &&
|
if (response['pull_data']['workouts'] != null) {
|
||||||
response['pull_data']['workouts'] != null) {
|
|
||||||
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
||||||
|
debugPrint(
|
||||||
|
'📥 Pulled ${pulledWorkouts.length} workouts from server.');
|
||||||
|
|
||||||
for (var wJson in pulledWorkouts) {
|
for (var wJson in pulledWorkouts) {
|
||||||
final serverId = wJson['id'];
|
final serverId = wJson['id'] as String;
|
||||||
var workout = await isar.workoutCollections
|
final existing = await (db.select(db.workouts)
|
||||||
.filter()
|
..where((w) => w.serverId.equals(serverId)))
|
||||||
.serverIdEqualTo(serverId)
|
.getSingleOrNull();
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
workout ??= WorkoutCollection();
|
final companion = WorkoutsCompanion(
|
||||||
|
serverId: Value(serverId),
|
||||||
|
cycleId: Value(wJson['cycle_id']),
|
||||||
|
userId: Value(wJson['user_id']),
|
||||||
|
week: Value(wJson['week']),
|
||||||
|
day: Value(wJson['day']),
|
||||||
|
completedAt:
|
||||||
|
Value(DateTime.tryParse(wJson['completed_at'] ?? '')),
|
||||||
|
xpEarned: Value(wJson['xp_earned'] ?? 0),
|
||||||
|
exercises: Value(wJson['exercises'] ?? []),
|
||||||
|
notes: Value(wJson['notes'] ?? ''),
|
||||||
|
isDirty: const Value(false),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
createdAt: existing == null
|
||||||
|
? Value(DateTime.now())
|
||||||
|
: const Value.absent(),
|
||||||
|
);
|
||||||
|
|
||||||
workout
|
if (existing != null) {
|
||||||
..serverId = serverId
|
await (db.update(db.workouts)
|
||||||
..cycleId = wJson['cycle_id']
|
..where((w) => w.id.equals(existing.id)))
|
||||||
..userId = wJson['user_id']
|
.write(companion);
|
||||||
..week = wJson['week']
|
} else {
|
||||||
..day = wJson['day']
|
await db.into(db.workouts).insert(companion);
|
||||||
..completedAt = DateTime.tryParse(wJson['completed_at'] ?? '')
|
}
|
||||||
..xpEarned = wJson['xp_earned'] ?? 0
|
}
|
||||||
..exercisesJson = jsonEncode(wJson['exercises'])
|
|
||||||
..isDirty = false
|
|
||||||
..updatedAt = DateTime.now();
|
|
||||||
|
|
||||||
await isar.workoutCollections.put(workout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -180,12 +218,11 @@ class SyncService {
|
||||||
value: response['server_timestamp'],
|
value: response['server_timestamp'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('✅ Sync completed successfully');
|
debugPrint('✅ Sync completed successfully');
|
||||||
} catch (e) {
|
} catch (e, stack) {
|
||||||
debugPrint('❌ Sync failed: $e');
|
debugPrint('❌ Sync failed: $e');
|
||||||
|
debugPrint(stack.toString());
|
||||||
} finally {
|
} finally {
|
||||||
_isSyncing = false;
|
_isSyncing = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,49 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import '../local/collections/cycle_collection.dart';
|
import '../local/app_database.dart';
|
||||||
import '../remote/api_client.dart';
|
import '../remote/api_client.dart';
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
import 'user_repository.dart';
|
import 'user_repository.dart';
|
||||||
import '../../../core/constants/app_constants.dart';
|
import '../../../core/constants/app_constants.dart';
|
||||||
import '../local/collections/workout_collection.dart';
|
|
||||||
|
|
||||||
final cycleRepositoryProvider = Provider<CycleRepository>((ref) {
|
final cycleRepositoryProvider = Provider<CycleRepository>((ref) {
|
||||||
final isar = ref.watch(isarProvider);
|
final db = ref.watch(appDatabaseProvider);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
return CycleRepository(isar: isar, apiClient: apiClient);
|
return CycleRepository(db: db, apiClient: apiClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
class CycleRepository {
|
class CycleRepository {
|
||||||
final Isar isar;
|
final AppDatabase db;
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
CycleRepository({required this.isar, required this.apiClient});
|
CycleRepository({required this.db, required this.apiClient});
|
||||||
|
|
||||||
Future<CycleCollection?> getCurrentCycle() async {
|
Future<CycleCollection?> getCurrentCycle() async {
|
||||||
return await isar.cycleCollections
|
return await (db.select(db.cycles)
|
||||||
.filter()
|
..where((c) => c.isActive.equals(true))
|
||||||
.isActiveEqualTo(true)
|
..limit(1))
|
||||||
.findFirst();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<CycleCollection>> getAllCycles() async {
|
Future<List<CycleCollection>> getAllCycles() async {
|
||||||
return await isar.cycleCollections.where().findAll();
|
return await db.select(db.cycles).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<CycleCollection> createCycle(Map<String, double> trainingMaxes) async {
|
Future<CycleCollection> createCycle(Map<String, double> trainingMaxes) async {
|
||||||
try {
|
return await db.transaction(() async {
|
||||||
final currentCycle = await getCurrentCycle();
|
final currentCycle = await getCurrentCycle();
|
||||||
if (currentCycle != null) {
|
if (currentCycle != null) {
|
||||||
currentCycle.isActive = false;
|
final updateOld = CyclesCompanion(
|
||||||
currentCycle.endDate = DateTime.now();
|
isActive: const Value(false),
|
||||||
await saveCycle(currentCycle);
|
endDate: Value(DateTime.now()),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
isDirty: const Value(true),
|
||||||
|
);
|
||||||
|
await (db.update(db.cycles)..where((c) => c.id.equals(currentCycle.id)))
|
||||||
|
.write(updateOld);
|
||||||
}
|
}
|
||||||
|
|
||||||
final allCycles = await getAllCycles();
|
final allCycles = await getAllCycles();
|
||||||
|
|
@ -50,34 +54,44 @@ class CycleRepository {
|
||||||
.reduce((a, b) => a > b ? a : b) +
|
.reduce((a, b) => a > b ? a : b) +
|
||||||
1;
|
1;
|
||||||
|
|
||||||
final userRepo = UserRepository(isar: isar, apiClient: ApiClient());
|
final user = await (db.select(db.users)..limit(1)).getSingleOrNull();
|
||||||
final user = await userRepo.getLocalUser();
|
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw Exception('No user found for cycle creation');
|
throw Exception('No user found for cycle creation');
|
||||||
}
|
}
|
||||||
|
|
||||||
final newCycle = CycleCollection()
|
final newCycleCompanion = CyclesCompanion(
|
||||||
..userId = user.serverId ?? user.id.toString()
|
userId: Value(user.serverId ?? user.id.toString()),
|
||||||
..cycleNumber = nextNumber
|
cycleNumber: Value(nextNumber),
|
||||||
..startDate = DateTime.now()
|
startDate: Value(DateTime.now()),
|
||||||
..isActive = true
|
isActive: const Value(true),
|
||||||
..trainingMaxesJson = jsonEncode(trainingMaxes)
|
trainingMaxes: Value(trainingMaxes),
|
||||||
..isDirty = true;
|
isDirty: const Value(true),
|
||||||
|
createdAt: Value(DateTime.now()),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
|
||||||
await saveCycle(newCycle);
|
final newId = await db.into(db.cycles).insert(newCycleCompanion);
|
||||||
|
var newCycle = await (db.select(db.cycles)
|
||||||
|
..where((c) => c.id.equals(newId)))
|
||||||
|
.getSingle();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await apiClient.createCycle(trainingMaxes);
|
final response = await apiClient.createCycle(trainingMaxes);
|
||||||
newCycle.serverId = response['id'];
|
await (db.update(db.cycles)..where((c) => c.id.equals(newId))).write(
|
||||||
newCycle.isDirty = false;
|
CyclesCompanion(
|
||||||
await saveCycle(newCycle);
|
serverId: Value(response['id']),
|
||||||
} catch (e) {}
|
isDirty: const Value(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
newCycle = await (db.select(db.cycles)
|
||||||
|
..where((c) => c.id.equals(newId)))
|
||||||
|
.getSingle();
|
||||||
|
} catch (e) {
|
||||||
|
// API Fehler ignorieren, wird später gesynct
|
||||||
|
}
|
||||||
|
|
||||||
return newCycle;
|
return newCycle;
|
||||||
} catch (e, stackTrace) {
|
});
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<CycleCollection> finishCycle() async {
|
Future<CycleCollection> finishCycle() async {
|
||||||
|
|
@ -87,24 +101,26 @@ class CycleRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString();
|
final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString();
|
||||||
|
final localCycleId = currentCycle.id.toString();
|
||||||
|
|
||||||
final completedMainWorkouts = await isar.workoutCollections
|
final workoutsQuery = db.select(db.workouts)
|
||||||
.filter()
|
..where((w) {
|
||||||
.weekLessThan(4)
|
final weekCheck = w.week.isSmallerThanValue(4);
|
||||||
.completedAtIsNotNull()
|
final completedCheck = w.completedAt.isNotNull();
|
||||||
.group((q) => q
|
final cycleCheck =
|
||||||
.cycleIdEqualTo(cycleIdRef)
|
w.cycleId.equals(cycleIdRef) | w.cycleId.equals(localCycleId);
|
||||||
.or()
|
return weekCheck & completedCheck & cycleCheck;
|
||||||
.cycleIdEqualTo(currentCycle.id.toString()))
|
});
|
||||||
.count();
|
|
||||||
|
final completedMainWorkouts = (await workoutsQuery.get()).length;
|
||||||
|
|
||||||
if (completedMainWorkouts < 9) {
|
if (completedMainWorkouts < 9) {
|
||||||
final missing = 9 - completedMainWorkouts;
|
final missing = 9 - completedMainWorkouts;
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Cycle incomplete! You still have $missing workouts left in the main phase (Weeks 1-3). Finish them before leveling up.');
|
'Cycle incomplete! You still have $missing workouts left in the main phase (Weeks 1-3). Finish them before leveling up.');
|
||||||
}
|
}
|
||||||
final currentTMs =
|
|
||||||
jsonDecode(currentCycle.trainingMaxesJson) as Map<String, dynamic>;
|
final currentTMs = currentCycle.trainingMaxes;
|
||||||
|
|
||||||
final newTMs = <String, double>{
|
final newTMs = <String, double>{
|
||||||
'squat': (currentTMs['squat'] as num?)?.toDouble() ?? 0.0,
|
'squat': (currentTMs['squat'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
|
@ -112,23 +128,27 @@ class CycleRepository {
|
||||||
'dip': (currentTMs['dip'] as num?)?.toDouble() ?? 0.0,
|
'dip': (currentTMs['dip'] as num?)?.toDouble() ?? 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
final week3Workouts = await isar.workoutCollections
|
final week3Workouts = await (db.select(db.workouts)
|
||||||
.filter()
|
..where((w) {
|
||||||
.weekEqualTo(3)
|
final weekCheck = w.week.equals(3);
|
||||||
.group((q) => q
|
final cycleCheck =
|
||||||
.cycleIdEqualTo(cycleIdRef)
|
w.cycleId.equals(cycleIdRef) | w.cycleId.equals(localCycleId);
|
||||||
.or()
|
return weekCheck & cycleCheck;
|
||||||
.cycleIdEqualTo(currentCycle.id.toString()))
|
}))
|
||||||
.findAll();
|
.get();
|
||||||
|
|
||||||
bool checkSuccess(String exerciseId) {
|
bool checkSuccess(String exerciseId) {
|
||||||
for (var workout in week3Workouts) {
|
for (var workout in week3Workouts) {
|
||||||
try {
|
try {
|
||||||
final exercises = jsonDecode(workout.exercisesJson) as List;
|
final exercises = workout.exercises;
|
||||||
for (var ex in exercises) {
|
|
||||||
|
for (var exData in exercises) {
|
||||||
|
final ex = exData as Map<String, dynamic>;
|
||||||
|
|
||||||
if (ex['exerciseId'] == exerciseId) {
|
if (ex['exerciseId'] == exerciseId) {
|
||||||
final sets = ex['sets'] as List;
|
final sets = ex['sets'] as List;
|
||||||
for (var s in sets) {
|
for (var sData in sets) {
|
||||||
|
final s = sData as Map<String, dynamic>;
|
||||||
if (s['isAmrap'] == true) {
|
if (s['isAmrap'] == true) {
|
||||||
final reps = s['repsActual'] as int? ?? 0;
|
final reps = s['repsActual'] as int? ?? 0;
|
||||||
if (reps >= 1) {
|
if (reps >= 1) {
|
||||||
|
|
@ -170,33 +190,23 @@ class CycleRepository {
|
||||||
try {
|
try {
|
||||||
await apiClient.finishCycle(currentCycle.serverId!);
|
await apiClient.finishCycle(currentCycle.serverId!);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fehler ignorieren, wird später gesynct
|
// Fehler ignorieren
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await createCycle(newTMs);
|
return await createCycle(newTMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveCycle(CycleCollection cycle) async {
|
Future<Map<String, double>> getCurrentTrainingMaxesAsync() async {
|
||||||
cycle.updatedAt = DateTime.now();
|
final cycle = await getCurrentCycle();
|
||||||
await isar.writeTxn(() async {
|
|
||||||
await isar.cycleCollections.put(cycle);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, double> getCurrentTrainingMaxes() {
|
|
||||||
final cycle =
|
|
||||||
isar.cycleCollections.filter().isActiveEqualTo(true).findFirstSync();
|
|
||||||
|
|
||||||
if (cycle != null) {
|
if (cycle != null) {
|
||||||
final tms = jsonDecode(cycle.trainingMaxesJson);
|
final tms = cycle.trainingMaxes;
|
||||||
return {
|
return {
|
||||||
'squat': (tms['squat'] as num?)?.toDouble() ?? 0.0,
|
'squat': (tms['squat'] as num?)?.toDouble() ?? 0.0,
|
||||||
'pullup': (tms['pullup'] as num?)?.toDouble() ?? 0.0,
|
'pullup': (tms['pullup'] as num?)?.toDouble() ?? 0.0,
|
||||||
'dip': (tms['dip'] as num?)?.toDouble() ?? 0.0,
|
'dip': (tms['dip'] as num?)?.toDouble() ?? 0.0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {'squat': 0.0, 'pullup': 0.0, 'dip': 0.0};
|
return {'squat': 0.0, 'pullup': 0.0, 'dip': 0.0};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,76 @@
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
import '../local/collections/cycle_collection.dart';
|
import '../local/app_database.dart';
|
||||||
import '../local/collections/user_collection.dart';
|
|
||||||
import '../local/collections/workout_collection.dart';
|
|
||||||
import '../remote/api_client.dart';
|
import '../remote/api_client.dart';
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
|
||||||
final userRepositoryProvider = Provider<UserRepository>((ref) {
|
final userRepositoryProvider = Provider<UserRepository>((ref) {
|
||||||
final isar = ref.watch(isarProvider);
|
final db = ref.watch(appDatabaseProvider);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
return UserRepository(isar: isar, apiClient: apiClient);
|
return UserRepository(db: db, apiClient: apiClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());
|
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());
|
||||||
|
|
||||||
class UserRepository {
|
class UserRepository {
|
||||||
final Isar isar;
|
final AppDatabase db;
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
final _storage = const FlutterSecureStorage(); // NEU: Instanz für Logout
|
||||||
|
|
||||||
UserRepository({required this.isar, required this.apiClient});
|
UserRepository({required this.db, required this.apiClient});
|
||||||
|
|
||||||
Future<UserCollection?> getLocalUser() async {
|
Future<UserCollection?> getLocalUser() async {
|
||||||
return await isar.userCollections.where().findFirst();
|
return await (db.select(db.users)..limit(1)).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveLocalUser(UserCollection user) async {
|
Future<void> saveLocalUser(UserCollection user) async {
|
||||||
user.updatedAt = DateTime.now();
|
final companion = user.toCompanion(true).copyWith(
|
||||||
await isar.writeTxn(() async {
|
updatedAt: Value(DateTime.now()),
|
||||||
await isar.userCollections.put(user);
|
);
|
||||||
});
|
await db.into(db.users).insertOnConflictUpdate(companion);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateXP(int xpToAdd) async {
|
Future<void> updateXP(int xpToAdd) async {
|
||||||
final user = await getLocalUser();
|
final user = await getLocalUser();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
user.xp += xpToAdd;
|
final newXp = user.xp + xpToAdd;
|
||||||
user.isDirty = true;
|
final companion = UsersCompanion(
|
||||||
await saveLocalUser(user);
|
xp: Value(newXp),
|
||||||
|
isDirty: const Value(true),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
await (db.update(db.users)..where((u) => u.id.equals(user.id)))
|
||||||
|
.write(companion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLevel(int newLevel) async {
|
Future<void> updateLevel(int newLevel) async {
|
||||||
final user = await getLocalUser();
|
final user = await getLocalUser();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
user.level = newLevel;
|
final companion = UsersCompanion(
|
||||||
user.isDirty = true;
|
level: Value(newLevel),
|
||||||
await saveLocalUser(user);
|
isDirty: const Value(true),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
await (db.update(db.users)..where((u) => u.id.equals(user.id)))
|
||||||
|
.write(companion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateBodyweight(double bodyweight) async {
|
Future<void> updateBodyweight(double bodyweight) async {
|
||||||
final user = await getLocalUser();
|
final user = await getLocalUser();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
user.currentBodyweight = bodyweight;
|
final companion = UsersCompanion(
|
||||||
user.isDirty = true;
|
currentBodyweight: Value(bodyweight),
|
||||||
await saveLocalUser(user);
|
isDirty: const Value(true),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
await (db.update(db.users)..where((u) => u.id.equals(user.id)))
|
||||||
|
.write(companion);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.updateBodyweight(bodyweight);
|
await apiClient.updateBodyweight(bodyweight);
|
||||||
|
|
@ -67,9 +81,13 @@ class UserRepository {
|
||||||
Future<void> updateInventory(Map<String, dynamic> inventory) async {
|
Future<void> updateInventory(Map<String, dynamic> inventory) async {
|
||||||
final user = await getLocalUser();
|
final user = await getLocalUser();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
user.inventorySettingsJson = jsonEncode(inventory);
|
final companion = UsersCompanion(
|
||||||
user.isDirty = true;
|
inventorySettings: Value(inventory),
|
||||||
await saveLocalUser(user);
|
isDirty: const Value(true),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
await (db.update(db.users)..where((u) => u.id.equals(user.id)))
|
||||||
|
.write(companion);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.updateInventory(inventory);
|
await apiClient.updateInventory(inventory);
|
||||||
|
|
@ -79,21 +97,7 @@ class UserRepository {
|
||||||
|
|
||||||
Future<UserCollection> login(String email, String password) async {
|
Future<UserCollection> login(String email, String password) async {
|
||||||
final response = await apiClient.login(email, password);
|
final response = await apiClient.login(email, password);
|
||||||
|
return _saveUserFromApi(response['record']);
|
||||||
final user = UserCollection()
|
|
||||||
..serverId = response['record']['id']
|
|
||||||
..email = response['record']['email']
|
|
||||||
..xp = response['record']['xp'] ?? 0
|
|
||||||
..level = response['record']['level'] ?? 1
|
|
||||||
..currentBodyweight =
|
|
||||||
(response['record']['current_bodyweight'] ?? 70.0).toDouble()
|
|
||||||
..inventorySettingsJson =
|
|
||||||
jsonEncode(response['record']['inventory_settings'] ?? {})
|
|
||||||
..avatarConfigJson = jsonEncode(response['record']['avatar_config'] ?? {})
|
|
||||||
..lastSyncAt = DateTime.now();
|
|
||||||
|
|
||||||
await saveLocalUser(user);
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UserCollection> register({
|
Future<UserCollection> register({
|
||||||
|
|
@ -111,48 +115,57 @@ class UserRepository {
|
||||||
);
|
);
|
||||||
|
|
||||||
final record = response['record'] ?? response;
|
final record = response['record'] ?? response;
|
||||||
|
final user = await _saveUserFromApi(record);
|
||||||
final user = UserCollection()
|
|
||||||
..serverId = record['id']?.toString()
|
|
||||||
..email = record['email']?.toString() ?? email
|
|
||||||
..xp = (record['xp'] as num?)?.toInt() ?? 0
|
|
||||||
..level = (record['level'] as num?)?.toInt() ?? 1
|
|
||||||
..currentBodyweight =
|
|
||||||
(record['current_bodyweight'] as num?)?.toDouble() ?? bodyweight
|
|
||||||
..inventorySettingsJson =
|
|
||||||
jsonEncode(record['inventory_settings'] ?? inventorySettings)
|
|
||||||
..avatarConfigJson = jsonEncode(record['avatar_config'] ??
|
|
||||||
{
|
|
||||||
'skin_tone': 'medium',
|
|
||||||
'hair_style': 'short_01',
|
|
||||||
'clothing': 'basic_tee',
|
|
||||||
'unlocked_items': ['basic_tee'],
|
|
||||||
})
|
|
||||||
..lastSyncAt = DateTime.now();
|
|
||||||
|
|
||||||
await saveLocalUser(user);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.login(email, password);
|
await apiClient.login(email, password);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
} catch (e, stackTrace) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<UserCollection> _saveUserFromApi(Map<String, dynamic> record) async {
|
||||||
|
await db.delete(db.users).go();
|
||||||
|
|
||||||
|
final companion = UsersCompanion(
|
||||||
|
serverId: Value(record['id']),
|
||||||
|
email: Value(record['email'] ?? ''),
|
||||||
|
xp: Value((record['xp'] as num?)?.toInt() ?? 0),
|
||||||
|
level: Value((record['level'] as num?)?.toInt() ?? 1),
|
||||||
|
currentBodyweight:
|
||||||
|
Value((record['current_bodyweight'] as num?)?.toDouble() ?? 70.0),
|
||||||
|
inventorySettings: Value(record['inventory_settings'] ?? {}),
|
||||||
|
avatarConfig: Value(record['avatar_config'] ?? {}),
|
||||||
|
lastSyncAt: Value(DateTime.now()),
|
||||||
|
isDirty: const Value(false),
|
||||||
|
createdAt: Value(DateTime.now()),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
|
||||||
|
final id = await db.into(db.users).insert(companion);
|
||||||
|
return (await (db.select(db.users)..where((u) => u.id.equals(id)))
|
||||||
|
.getSingle());
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
await apiClient.logout();
|
await apiClient.logout();
|
||||||
await isar.writeTxn(() async {
|
|
||||||
await isar.userCollections.clear();
|
await _storage.delete(key: AppConstants.keyLastSync);
|
||||||
|
|
||||||
|
await db.transaction(() async {
|
||||||
|
await db.delete(db.users).go();
|
||||||
|
await db.delete(db.cycles).go();
|
||||||
|
await db.delete(db.workouts).go();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> getInventorySettings() {
|
Future<Map<String, dynamic>> getInventorySettingsAsync() async {
|
||||||
final user = isar.userCollections.where().findFirstSync();
|
final user = await getLocalUser();
|
||||||
if (user?.inventorySettingsJson != null) {
|
if (user?.inventorySettings != null) {
|
||||||
return jsonDecode(user!.inventorySettingsJson!);
|
return user!.inventorySettings!;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'bar_weight': 20.0,
|
'bar_weight': 20.0,
|
||||||
|
|
@ -161,14 +174,14 @@ class UserRepository {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
List<double> getAvailablePlates() {
|
Future<List<double>> getAvailablePlates() async {
|
||||||
final inventory = getInventorySettings();
|
final inventory = await getInventorySettingsAsync();
|
||||||
final plates = inventory['plates'] as List?;
|
final plates = inventory['plates'] as List?;
|
||||||
return plates?.map((e) => (e as num).toDouble()).toList() ?? [];
|
return plates?.map((e) => (e as num).toDouble()).toList() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
double getBarWeight() {
|
Future<double> getBarWeight() async {
|
||||||
final inventory = getInventorySettings();
|
final inventory = await getInventorySettingsAsync();
|
||||||
return (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
return (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,17 +217,17 @@ class UserRepository {
|
||||||
"Server connection required to reset progress. Please try again when online.");
|
"Server connection required to reset progress. Please try again when online.");
|
||||||
}
|
}
|
||||||
|
|
||||||
user.xp = 0;
|
final companion = UsersCompanion(
|
||||||
user.level = 1;
|
xp: const Value(0),
|
||||||
|
level: const Value(1),
|
||||||
|
isDirty: const Value(false),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
await (db.update(db.users)..where((u) => u.id.equals(user.id)))
|
||||||
|
.write(companion);
|
||||||
|
|
||||||
user.isDirty = false;
|
await db.delete(db.cycles).go();
|
||||||
|
await db.delete(db.workouts).go();
|
||||||
await isar.writeTxn(() async {
|
|
||||||
await isar.userCollections.put(user);
|
|
||||||
|
|
||||||
await isar.cycleCollections.clear();
|
|
||||||
await isar.workoutCollections.clear();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,44 @@
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'dart:convert';
|
import '../local/app_database.dart';
|
||||||
|
|
||||||
import '../local/collections/workout_collection.dart';
|
|
||||||
import '../remote/api_client.dart';
|
import '../remote/api_client.dart';
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
import 'user_repository.dart';
|
import 'user_repository.dart';
|
||||||
|
|
||||||
final workoutRepositoryProvider = Provider<WorkoutRepository>((ref) {
|
final workoutRepositoryProvider = Provider<WorkoutRepository>((ref) {
|
||||||
final isar = ref.watch(isarProvider);
|
final db = ref.watch(appDatabaseProvider);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
return WorkoutRepository(isar: isar, apiClient: apiClient);
|
return WorkoutRepository(db: db, apiClient: apiClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
class WorkoutRepository {
|
class WorkoutRepository {
|
||||||
final Isar isar;
|
final AppDatabase db;
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
WorkoutRepository({required this.isar, required this.apiClient});
|
WorkoutRepository({required this.db, required this.apiClient});
|
||||||
|
|
||||||
Future<List<WorkoutCollection>> getAllWorkouts() async {
|
Future<List<WorkoutCollection>> getAllWorkouts() async {
|
||||||
return await isar.workoutCollections.where().findAll();
|
return await db.select(db.workouts).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<WorkoutCollection>> getWorkoutsForCycle(String cycleId) async {
|
Future<List<WorkoutCollection>> getWorkoutsForCycle(String cycleId) async {
|
||||||
return await isar.workoutCollections
|
return await (db.select(db.workouts)
|
||||||
.filter()
|
..where((w) => w.cycleId.equals(cycleId)))
|
||||||
.cycleIdEqualTo(cycleId)
|
.get();
|
||||||
.findAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<WorkoutCollection>> getCompletedWorkouts(String userId) async {
|
Future<List<WorkoutCollection>> getCompletedWorkouts(String userId) async {
|
||||||
return await isar.workoutCollections
|
return await (db.select(db.workouts)
|
||||||
.filter()
|
..where((w) => w.userId.equals(userId) & w.completedAt.isNotNull()))
|
||||||
.userIdEqualTo(userId)
|
.get();
|
||||||
.completedAtIsNotNull()
|
|
||||||
.findAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveWorkout(WorkoutCollection workout) async {
|
Future<void> saveWorkout(WorkoutCollection workout) async {
|
||||||
workout.updatedAt = DateTime.now();
|
final companion = workout.toCompanion(true).copyWith(
|
||||||
workout.isDirty = true;
|
updatedAt: Value(DateTime.now()),
|
||||||
await isar.writeTxn(() async {
|
isDirty: const Value(true),
|
||||||
await isar.workoutCollections.put(workout);
|
);
|
||||||
});
|
await db.into(db.workouts).insertOnConflictUpdate(companion);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<WorkoutCollection> createWorkout({
|
Future<WorkoutCollection> createWorkout({
|
||||||
|
|
@ -51,27 +46,42 @@ class WorkoutRepository {
|
||||||
required String cycleId,
|
required String cycleId,
|
||||||
required int week,
|
required int week,
|
||||||
required int day,
|
required int day,
|
||||||
required String exercisesJson,
|
required List<dynamic> exercises,
|
||||||
}) async {
|
}) async {
|
||||||
final workout = WorkoutCollection()
|
final companion = WorkoutsCompanion(
|
||||||
..userId = userId
|
userId: Value(userId),
|
||||||
..cycleId = cycleId
|
cycleId: Value(cycleId),
|
||||||
..week = week
|
week: Value(week),
|
||||||
..day = day
|
day: Value(day),
|
||||||
..exercisesJson = exercisesJson
|
exercises: Value(exercises),
|
||||||
..scheduledDate = DateTime.now();
|
scheduledDate: Value(DateTime.now()),
|
||||||
|
xpEarned: const Value(0),
|
||||||
|
notes: const Value(''),
|
||||||
|
isDirty: const Value(true),
|
||||||
|
createdAt: Value(DateTime.now()),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
|
||||||
await saveWorkout(workout);
|
final id = await db.into(db.workouts).insert(companion);
|
||||||
return workout;
|
return await (db.select(db.workouts)..where((w) => w.id.equals(id)))
|
||||||
|
.getSingle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> completeWorkout(
|
Future<void> completeWorkout(
|
||||||
WorkoutCollection workout, {
|
WorkoutCollection workout, {
|
||||||
required int xpEarned,
|
required int xpEarned,
|
||||||
}) async {
|
}) async {
|
||||||
workout.completedAt = DateTime.now();
|
final companion = WorkoutsCompanion(
|
||||||
workout.xpEarned = xpEarned;
|
id: Value(workout.id),
|
||||||
await saveWorkout(workout);
|
completedAt: Value(DateTime.now()),
|
||||||
|
xpEarned: Value(xpEarned),
|
||||||
|
exercises: Value(workout.exercises),
|
||||||
|
isDirty: const Value(true),
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
|
||||||
|
await (db.update(db.workouts)..where((w) => w.id.equals(workout.id)))
|
||||||
|
.write(companion);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<WorkoutCollection?> getWorkoutByWeekDay({
|
Future<WorkoutCollection?> getWorkoutByWeekDay({
|
||||||
|
|
@ -80,16 +90,17 @@ class WorkoutRepository {
|
||||||
required int week,
|
required int week,
|
||||||
required int day,
|
required int day,
|
||||||
}) async {
|
}) async {
|
||||||
return await isar.workoutCollections
|
return await (db.select(db.workouts)
|
||||||
.filter()
|
..where((w) {
|
||||||
.weekEqualTo(week)
|
final weekDayCheck = w.week.equals(week) & w.day.equals(day);
|
||||||
.dayEqualTo(day)
|
|
||||||
.group((q) {
|
Expression<bool> cycleCheck = w.cycleId.equals(cycleId);
|
||||||
var query = q.cycleIdEqualTo(cycleId);
|
|
||||||
if (localCycleId != null) {
|
if (localCycleId != null) {
|
||||||
query = query.or().cycleIdEqualTo(localCycleId);
|
cycleCheck = cycleCheck | w.cycleId.equals(localCycleId);
|
||||||
}
|
}
|
||||||
return query;
|
|
||||||
}).findFirst();
|
return weekDayCheck & cycleCheck;
|
||||||
|
}))
|
||||||
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
132
pubspec.lock
132
pubspec.lock
|
|
@ -5,26 +5,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
|
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "61.0.0"
|
version: "67.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
|
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.13.0"
|
version: "6.4.1"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer_plugin
|
name: analyzer_plugin
|
||||||
sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d
|
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.2"
|
version: "0.11.3"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -145,6 +145,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
charcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: charcode
|
||||||
|
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -153,6 +161,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.4"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.2"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -217,14 +233,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
dartx:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dartx
|
|
||||||
sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.0"
|
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -241,6 +249,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
drift:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: drift
|
||||||
|
sha256: df027d168a2985a2e9da900adeba2ab0136f0d84436592cf3cd5135f82c8579c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.21.0"
|
||||||
|
drift_dev:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: drift_dev
|
||||||
|
sha256: "623649abe932fc17bd32e578e7e05f7ac5e7dd0b33e6c8669a0634105d1389bf"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.21.2"
|
||||||
|
drift_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: drift_flutter
|
||||||
|
sha256: "0bc2f1dde59e59cedde0df67a4ff7bd8f0d42274f18b50bf7e7dae7ca3d77801"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
equatable:
|
equatable:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -480,30 +512,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
isar:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: isar
|
|
||||||
sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.0+1"
|
|
||||||
isar_flutter_libs:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: isar_flutter_libs
|
|
||||||
sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.0+1"
|
|
||||||
isar_generator:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: isar_generator
|
|
||||||
sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.0+1"
|
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -744,6 +752,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
|
recase:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: recase
|
||||||
|
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -933,6 +949,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.0"
|
||||||
|
sqlite3:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3
|
||||||
|
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.4"
|
||||||
|
sqlite3_flutter_libs:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqlite3_flutter_libs
|
||||||
|
sha256: "1e800ebe7f85a80a66adacaa6febe4d5f4d8b75f244e9838a27cb2ffc7aec08d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.41"
|
||||||
|
sqlparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlparser
|
||||||
|
sha256: d77749237609784e337ec36c979d41f6f38a7b279df98622ae23929c8eb954a4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.39.2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -997,14 +1037,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.7"
|
||||||
time:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: time
|
|
||||||
sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.5"
|
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1125,14 +1157,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "6.6.1"
|
||||||
xxh3:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: xxh3
|
|
||||||
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.0"
|
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ dependencies:
|
||||||
riverpod_annotation: ^2.3.5
|
riverpod_annotation: ^2.3.5
|
||||||
|
|
||||||
# Local Database
|
# Local Database
|
||||||
isar: ^3.1.0+1
|
drift: ^2.16.0
|
||||||
isar_flutter_libs: ^3.1.0+1
|
drift_flutter: ^0.1.0
|
||||||
|
sqlite3_flutter_libs: ^0.5.20
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
|
|
||||||
# Networking
|
# Networking
|
||||||
|
|
@ -52,7 +53,7 @@ dev_dependencies:
|
||||||
# Code Generation
|
# Code Generation
|
||||||
build_runner: ^2.4.9
|
build_runner: ^2.4.9
|
||||||
riverpod_generator: ^2.4.0
|
riverpod_generator: ^2.4.0
|
||||||
isar_generator: ^3.1.0+1
|
drift_dev: ^2.16.0
|
||||||
freezed: ^2.5.2
|
freezed: ^2.5.2
|
||||||
json_serializable: ^6.8.0
|
json_serializable: ^6.8.0
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue