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/services.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/shared/data/local/collections/user_collection.dart';
|
||||
import 'src/shared/data/local/collections/cycle_collection.dart';
|
||||
import 'src/shared/data/local/collections/workout_collection.dart';
|
||||
import 'src/shared/data/local/app_database.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
|
@ -17,19 +12,15 @@ void main() async {
|
|||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final isar = await Isar.open(
|
||||
[UserCollectionSchema, CycleCollectionSchema, WorkoutCollectionSchema],
|
||||
directory: dir.path,
|
||||
name: 'slrpg_db',
|
||||
);
|
||||
final database = AppDatabase();
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [isarProvider.overrideWithValue(isar)],
|
||||
overrides: [appDatabaseProvider.overrideWithValue(database)],
|
||||
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) {
|
||||
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(
|
||||
SnackBar(
|
||||
content: Text('Login failed: ${e.toString()}'),
|
||||
content: Text(message),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -70,7 +82,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
|
|
@ -85,7 +96,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Text(
|
||||
'WELCOME BACK',
|
||||
style: Theme.of(context).textTheme.displayMedium,
|
||||
|
|
@ -98,7 +108,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
|
|
@ -117,7 +126,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
|
|
@ -146,7 +154,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleLogin,
|
||||
child: _isLoading
|
||||
|
|
@ -161,7 +168,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||
: const Text('LOGIN'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../core/theme/app_theme.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/presentation/widgets/avatar_editor.dart';
|
||||
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class ProfileScreen extends ConsumerStatefulWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
|
@ -46,6 +46,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
await ref
|
||||
.read(userRepositoryProvider)
|
||||
.updateBodyweight(_currentBodyweight);
|
||||
|
||||
if (_user != null) {
|
||||
_user = _user!.copyWith(currentBodyweight: _currentBodyweight);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _hasChanges = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -163,8 +168,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
}
|
||||
|
||||
void _showAvatarEditor() {
|
||||
final currentConfig = _user?.avatarConfigJson != null
|
||||
? AvatarConfig.fromJson(jsonDecode(_user!.avatarConfigJson!))
|
||||
final currentConfig = _user?.avatarConfig != null
|
||||
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
||||
: const AvatarConfig();
|
||||
|
||||
showModalBottomSheet(
|
||||
|
|
@ -199,8 +204,13 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
).then((result) async {
|
||||
if (result is AvatarConfig) {
|
||||
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!);
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
|
|
@ -210,8 +220,8 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userRepo = ref.watch(userRepositoryProvider);
|
||||
final avatarConfig = _user?.avatarConfigJson != null
|
||||
? AvatarConfig.fromJson(jsonDecode(_user!.avatarConfigJson!))
|
||||
final avatarConfig = _user?.avatarConfig != null
|
||||
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
||||
: const AvatarConfig();
|
||||
|
||||
return Scaffold(
|
||||
|
|
|
|||
|
|
@ -6,21 +6,20 @@ import 'package:go_router/go_router.dart';
|
|||
|
||||
import '../../../../core/constants/asset_paths.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/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/domain/entities/exercise.dart';
|
||||
import '../../../../shared/domain/logic/wendler_calculator.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 '../../../gamification/domain/entities/avatar_config.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 {
|
||||
const HubScreen({super.key});
|
||||
|
|
@ -41,7 +40,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_runSync();
|
||||
});
|
||||
|
|
@ -104,7 +102,10 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
CycleCollection cycle, UserCollection user) async {
|
||||
try {
|
||||
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 targetDay = 1;
|
||||
|
|
@ -138,8 +139,6 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
return;
|
||||
}
|
||||
|
||||
final trainingMaxes = cycleRepo.getCurrentTrainingMaxes();
|
||||
|
||||
var workout = await workoutRepo.getWorkoutByWeekDay(
|
||||
cycleId: cycleRefId,
|
||||
localCycleId: localCycleId,
|
||||
|
|
@ -162,7 +161,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
cycleId: cycleRefId,
|
||||
week: targetWeek,
|
||||
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 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();
|
||||
|
||||
if (user == null) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.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/workout_repository.dart';
|
||||
import '../../../../shared/data/local/collections/workout_collection.dart';
|
||||
import '../../../../shared/domain/entities/exercise.dart';
|
||||
import '../../../../shared/domain/entities/workout_set.dart';
|
||||
|
||||
class HistoryScreen extends ConsumerStatefulWidget {
|
||||
const HistoryScreen({super.key});
|
||||
|
|
@ -27,13 +25,11 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
|||
if (user == null) return [];
|
||||
|
||||
final userId = user.serverId ?? user.id.toString();
|
||||
return workoutRepo.getCompletedWorkouts(userId); // ID übergeben
|
||||
return workoutRepo.getCompletedWorkouts(userId);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final workoutRepo = ref.watch(workoutRepositoryProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
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!));
|
||||
|
||||
return ListView.builder(
|
||||
|
|
@ -98,8 +94,10 @@ class _WorkoutHistoryCard extends StatelessWidget {
|
|||
|
||||
List<Exercise> _parseExercises() {
|
||||
try {
|
||||
final List<dynamic> jsonList = jsonDecode(workout.exercisesJson);
|
||||
return jsonList.map((json) => Exercise.fromJson(json)).toList();
|
||||
final List<dynamic> list = workout.exercises;
|
||||
return list
|
||||
.map((json) => Exercise.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing workout history: $e');
|
||||
return [];
|
||||
|
|
@ -237,10 +235,10 @@ class _ExerciseDetailRow extends StatelessWidget {
|
|||
Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
columnWidths: const {
|
||||
0: FlexColumnWidth(1), // Set
|
||||
1: FlexColumnWidth(2), // Weight
|
||||
2: FlexColumnWidth(2), // Reps
|
||||
3: FlexColumnWidth(1), // Type (AMRAP/FSL)
|
||||
0: FlexColumnWidth(1),
|
||||
1: FlexColumnWidth(2),
|
||||
2: FlexColumnWidth(2),
|
||||
3: FlexColumnWidth(1),
|
||||
},
|
||||
children: exercise.sets.where((s) => s.completed).map((set) {
|
||||
return TableRow(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
|||
|
||||
Future<void> _loadCurrentInventory() async {
|
||||
final userRepo = ref.read(userRepositoryProvider);
|
||||
final inventory = userRepo.getInventorySettings();
|
||||
final inventory = await userRepo.getInventorySettingsAsync();
|
||||
|
||||
final barWeight = (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
||||
|
||||
|
|
@ -62,7 +62,8 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
|||
};
|
||||
|
||||
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)) {
|
||||
bandMap[color] = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/constants/app_constants.dart';
|
||||
import '../../../../shared/data/repositories/user_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/presentation/widgets/avatar_editor.dart';
|
||||
import 'bodyweight_input_screen.dart';
|
||||
|
|
@ -41,10 +41,12 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
|||
inventorySettings: inventorySettings,
|
||||
);
|
||||
} else {
|
||||
user.currentBodyweight =
|
||||
onboardingData['bodyweight'] ?? user.currentBodyweight;
|
||||
user.inventorySettingsJson = jsonEncode(inventorySettings);
|
||||
user.isDirty = true;
|
||||
user = user.copyWith(
|
||||
currentBodyweight:
|
||||
onboardingData['bodyweight'] ?? user.currentBodyweight,
|
||||
inventorySettings: Value(inventorySettings),
|
||||
isDirty: true,
|
||||
);
|
||||
await userRepo.saveLocalUser(user);
|
||||
|
||||
try {
|
||||
|
|
@ -55,8 +57,10 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
user!.avatarConfigJson = jsonEncode(_config.toJson());
|
||||
user.isDirty = true;
|
||||
user = user.copyWith(
|
||||
avatarConfig: Value(_config.toJson()),
|
||||
isDirty: true,
|
||||
);
|
||||
await userRepo.saveLocalUser(user);
|
||||
|
||||
final trainingMaxes =
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.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/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/workout_repository.dart';
|
||||
import '../../../../shared/domain/entities/exercise.dart';
|
||||
|
|
@ -55,19 +53,15 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
for (var workout in allWorkouts) {
|
||||
if (workout.completedAt == null) continue;
|
||||
|
||||
List<dynamic> exercisesJson = [];
|
||||
try {
|
||||
exercisesJson = jsonDecode(workout.exercisesJson);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
final exercisesList = workout.exercises;
|
||||
|
||||
double max1RM = 0.0;
|
||||
double sessionVolume = 0.0;
|
||||
bool foundExercise = false;
|
||||
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);
|
||||
|
||||
if (exercise.exerciseId == _selectedExercise) {
|
||||
|
|
@ -147,12 +141,10 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
try {
|
||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||
|
||||
final oldTMs =
|
||||
jsonDecode(currentCycle.trainingMaxesJson) as Map<String, dynamic>;
|
||||
final oldTMs = currentCycle.trainingMaxes;
|
||||
|
||||
final newCycle = await cycleRepo.finishCycle();
|
||||
final newTMs =
|
||||
jsonDecode(newCycle.trainingMaxesJson) as Map<String, dynamic>;
|
||||
final newTMs = newCycle.trainingMaxes;
|
||||
|
||||
if (mounted) {
|
||||
await showDialog(
|
||||
|
|
@ -215,7 +207,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
Text(
|
||||
'Progress Analysis',
|
||||
style: Theme.of(context)
|
||||
|
|
@ -224,7 +215,6 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
?.copyWith(color: AppTheme.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
|
|
@ -250,14 +240,11 @@ class _StatsScreenState extends ConsumerState<StatsScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_isChartLoading
|
||||
? const SizedBox(
|
||||
height: 250,
|
||||
child: Center(child: CircularProgressIndicator()))
|
||||
: ProgressChart(data: _chartData),
|
||||
|
||||
// (Optional: Range Selector unten drunter '1M', '3M', '1Y'...)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -275,7 +262,8 @@ class _CurrentCycleCard extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tms = jsonDecode(cycle.trainingMaxesJson) as Map<String, dynamic>;
|
||||
// Drift: Direct access
|
||||
final tms = cycle.trainingMaxes;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
|
|
@ -380,7 +368,7 @@ class _CycleFinishDialog extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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 Divider(),
|
||||
const SizedBox(height: 8),
|
||||
|
|
@ -388,12 +376,17 @@ class _CycleFinishDialog extends StatelessWidget {
|
|||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_DiffRow(
|
||||
name: 'Squat', oldVal: oldTMs['squat'], newVal: newTMs['squat']),
|
||||
name: 'Squat',
|
||||
oldVal: (oldTMs['squat'] as num).toDouble(),
|
||||
newVal: (newTMs['squat'] as num).toDouble()),
|
||||
_DiffRow(
|
||||
name: 'Pull-up',
|
||||
oldVal: oldTMs['pullup'],
|
||||
newVal: newTMs['pullup']),
|
||||
_DiffRow(name: 'Dip', oldVal: oldTMs['dip'], newVal: newTMs['dip']),
|
||||
oldVal: (oldTMs['pullup'] as num).toDouble(),
|
||||
newVal: (newTMs['pullup'] as num).toDouble()),
|
||||
_DiffRow(
|
||||
name: 'Dip',
|
||||
oldVal: (oldTMs['dip'] as num).toDouble(),
|
||||
newVal: (newTMs['dip'] as num).toDouble()),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../../core/constants/asset_paths.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
|
|
@ -127,16 +126,14 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
||||
final trainingMaxes = cycleRepo.getCurrentTrainingMaxes();
|
||||
final exercises = <Exercise>[];
|
||||
|
||||
final exerciseConfigs = _getExerciseConfig(widget.day);
|
||||
|
||||
for (final config in exerciseConfigs) {
|
||||
|
|
@ -145,7 +142,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
final type = config['type'] as ExerciseType;
|
||||
final isMain = config['isMain'] as bool;
|
||||
|
||||
final tm = trainingMaxes[id] ?? 0.0;
|
||||
final tm = trainingMaxesMap[id] ?? 0.0;
|
||||
List<WorkoutSet> sets = [];
|
||||
|
||||
if (isMain) {
|
||||
|
|
@ -175,14 +172,16 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_exercises = exercises;
|
||||
_isLoading = false;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_exercises = exercises;
|
||||
_isLoading = false;
|
||||
|
||||
if (exercises.isNotEmpty && exercises.first.sets.isNotEmpty) {
|
||||
_repsCompleted = exercises.first.sets.first.repsTarget;
|
||||
}
|
||||
});
|
||||
if (exercises.isNotEmpty && exercises.first.sets.isNotEmpty) {
|
||||
_repsCompleted = exercises.first.sets.first.repsTarget;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _completeSet() {
|
||||
|
|
@ -286,9 +285,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
cycleId: cycleIdRef, week: widget.week, day: widget.day);
|
||||
|
||||
if (workout != null) {
|
||||
workout.exercisesJson =
|
||||
jsonEncode(_exercises.map((e) => e.toJson()).toList());
|
||||
await workoutRepo.completeWorkout(workout, xpEarned: xpEarned);
|
||||
final updatedExercises = _exercises.map((e) => e.toJson()).toList();
|
||||
final updatedWorkout = workout.copyWith(exercises: updatedExercises);
|
||||
|
||||
await workoutRepo.completeWorkout(updatedWorkout, xpEarned: xpEarned);
|
||||
|
||||
ref.read(syncServiceProvider).sync();
|
||||
}
|
||||
|
|
@ -355,7 +355,7 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'The monsters tremble at your new power.', // Story Flavor
|
||||
'The monsters tremble at your new power.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.black54, fontStyle: FontStyle.italic),
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
|
|
@ -396,101 +407,115 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
|||
|
||||
final currentExercise = _exercises[_currentExerciseIndex];
|
||||
final currentSet = currentExercise.sets[_currentSetIndex];
|
||||
final userRepo = ref.watch(userRepositoryProvider);
|
||||
|
||||
final totalHP = _exercises.fold<int>(
|
||||
0,
|
||||
(sum, ex) => sum + ex.sets.fold<int>(0, (s, set) => s + set.repsTarget),
|
||||
);
|
||||
return FutureBuilder<Map<String, dynamic>>(
|
||||
future: ref.read(userRepositoryProvider).getInventorySettingsAsync(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData)
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()));
|
||||
|
||||
final completedHP = _exercises.take(_currentExerciseIndex).fold<int>(
|
||||
0,
|
||||
(sum, ex) =>
|
||||
sum + ex.sets.fold<int>(0, (s, set) => s + set.repsActual),
|
||||
) +
|
||||
currentExercise.sets
|
||||
.take(_currentSetIndex)
|
||||
.fold<int>(0, (sum, set) => sum + set.repsActual);
|
||||
final inventory = snapshot.data!;
|
||||
|
||||
final isBodyweight = currentExercise.exerciseId != 'squat';
|
||||
final barWeight = isBodyweight
|
||||
? currentExercise.bodyweightAtSession
|
||||
: userRepo.getBarWeight();
|
||||
final availablePlates = userRepo.getAvailablePlates();
|
||||
final inventory = userRepo.getInventorySettings();
|
||||
final bandsList =
|
||||
(inventory['bands'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
final totalHP = _exercises.fold<int>(
|
||||
0,
|
||||
(sum, ex) =>
|
||||
sum + ex.sets.fold<int>(0, (s, set) => s + set.repsTarget),
|
||||
);
|
||||
|
||||
final Map<String, double> availableBands = {};
|
||||
for (var band in bandsList) {
|
||||
final color = band['color'] as String;
|
||||
final resistance = (band['resistance_kg'] as num).toDouble();
|
||||
if (band['count'] as int > 0) {
|
||||
availableBands[color] = resistance;
|
||||
}
|
||||
}
|
||||
final completedHP = _exercises.take(_currentExerciseIndex).fold<int>(
|
||||
0,
|
||||
(sum, ex) =>
|
||||
sum +
|
||||
ex.sets.fold<int>(0, (s, set) => s + set.repsActual),
|
||||
) +
|
||||
currentExercise.sets
|
||||
.take(_currentSetIndex)
|
||||
.fold<int>(0, (sum, set) => sum + set.repsActual);
|
||||
|
||||
final plateResult = PlateCalculator.calculate(
|
||||
targetWeight: currentSet.targetWeightTotal,
|
||||
barWeight: barWeight,
|
||||
availablePlates: availablePlates,
|
||||
availableBands: availableBands,
|
||||
isTwoSided: !isBodyweight,
|
||||
);
|
||||
final isBodyweight = currentExercise.exerciseId != 'squat';
|
||||
final barWeight = isBodyweight
|
||||
? currentExercise.bodyweightAtSession
|
||||
: (inventory['bar_weight'] as num?)?.toDouble() ?? 20.0;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Week ${widget.week} - Day ${widget.day}'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Abandon Raid?'),
|
||||
content: const Text('Your progress will not be saved.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('CANCEL'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.go('/hub');
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppTheme.errorColor),
|
||||
child: const Text('ABANDON'),
|
||||
),
|
||||
],
|
||||
final platesList = (inventory['plates'] as List?)
|
||||
?.map((e) => (e as num).toDouble())
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
final bandsList =
|
||||
(inventory['bands'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
final Map<String, double> availableBands = {};
|
||||
for (var band in bandsList) {
|
||||
final color = band['color'] as String;
|
||||
final resistance = (band['resistance_kg'] as num).toDouble();
|
||||
if (band['count'] as int > 0) {
|
||||
availableBands[color] = resistance;
|
||||
}
|
||||
}
|
||||
|
||||
final plateResult = PlateCalculator.calculate(
|
||||
targetWeight: currentSet.targetWeightTotal,
|
||||
barWeight: barWeight,
|
||||
availablePlates: platesList,
|
||||
availableBands: availableBands,
|
||||
isTwoSided: !isBodyweight,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Week ${widget.week} - Day ${widget.day}'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Abandon Raid?'),
|
||||
content: const Text('Your progress will not be saved.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('CANCEL'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.go('/hub');
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppTheme.errorColor),
|
||||
child: const Text('ABANDON'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
AssetPaths.bgUndergroundGym,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
AssetPaths.bgUndergroundGym,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: _isResting
|
||||
? _buildRestScreen()
|
||||
: _buildWorkoutScreen(currentExercise, currentSet,
|
||||
plateResult, completedHP, totalHP),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: _isResting
|
||||
? _buildRestScreen()
|
||||
: _buildWorkoutScreen(currentExercise, currentSet, plateResult,
|
||||
completedHP, totalHP),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
final minutes = 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 'package:flutter/foundation.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 '../../../../main.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
import '../local/collections/user_collection.dart';
|
||||
import '../local/collections/cycle_collection.dart';
|
||||
import '../local/collections/workout_collection.dart';
|
||||
import 'api_client.dart';
|
||||
import '../local/app_database.dart';
|
||||
import '../repositories/user_repository.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
final syncServiceProvider = Provider<SyncService>((ref) {
|
||||
final isar = ref.watch(isarProvider);
|
||||
final db = ref.watch(appDatabaseProvider);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
return SyncService(isar: isar, apiClient: apiClient);
|
||||
return SyncService(db: db, apiClient: apiClient);
|
||||
});
|
||||
|
||||
class SyncService {
|
||||
final Isar isar;
|
||||
final AppDatabase db;
|
||||
final ApiClient apiClient;
|
||||
final _storage = const FlutterSecureStorage();
|
||||
bool _isSyncing = false;
|
||||
|
||||
SyncService({required this.isar, required this.apiClient});
|
||||
SyncService({required this.db, required this.apiClient});
|
||||
|
||||
Future<void> sync() async {
|
||||
if (_isSyncing) return;
|
||||
|
|
@ -32,160 +30,199 @@ class SyncService {
|
|||
|
||||
try {
|
||||
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) {
|
||||
try {
|
||||
if (cycle.serverId == null) {
|
||||
debugPrint(
|
||||
'📤 Pushing new cycle ${cycle.cycleNumber} to server...');
|
||||
|
||||
Map<String, double> tmsMap = {};
|
||||
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 tmsMap = cycle.trainingMaxes
|
||||
.map((k, v) => MapEntry(k, (v as num).toDouble()));
|
||||
|
||||
final response = await apiClient.createCycle(tmsMap);
|
||||
final newServerId = response['id'];
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
cycle.serverId = newServerId;
|
||||
cycle.isDirty = false;
|
||||
await isar.cycleCollections.put(cycle);
|
||||
await db.transaction(() async {
|
||||
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||
.write(
|
||||
CyclesCompanion(
|
||||
serverId: Value(newServerId),
|
||||
isDirty: const Value(false),
|
||||
),
|
||||
);
|
||||
|
||||
final oldLocalIdRef = cycle.id.toString();
|
||||
|
||||
final orphanWorkouts = await isar.workoutCollections
|
||||
.filter()
|
||||
.cycleIdEqualTo(oldLocalIdRef)
|
||||
.findAll();
|
||||
|
||||
for (var w in orphanWorkouts) {
|
||||
w.cycleId = newServerId;
|
||||
w.isDirty = true;
|
||||
await isar.workoutCollections.put(w);
|
||||
debugPrint('🔗 Relinked workout ${w.id} to cycle $newServerId');
|
||||
}
|
||||
await (db.update(db.workouts)
|
||||
..where((w) => w.cycleId.equals(oldLocalIdRef)))
|
||||
.write(
|
||||
WorkoutsCompanion(
|
||||
cycleId: Value(newServerId),
|
||||
isDirty: const Value(true),
|
||||
),
|
||||
);
|
||||
debugPrint(
|
||||
'🔗 Relinked workouts from local cycle $oldLocalIdRef to server $newServerId');
|
||||
});
|
||||
} else {
|
||||
await isar.writeTxn(() async {
|
||||
cycle.isDirty = false;
|
||||
await isar.cycleCollections.put(cycle);
|
||||
});
|
||||
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||
.write(
|
||||
const CyclesCompanion(isDirty: Value(false)),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Failed to sync cycle: $e');
|
||||
return;
|
||||
debugPrint('❌ Failed to sync cycle ${cycle.id}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final dirtyUser =
|
||||
await isar.userCollections.filter().isDirtyEqualTo(true).findFirst();
|
||||
final dirtyUser = await (db.select(db.users)
|
||||
..where((u) => u.isDirty.equals(true)))
|
||||
.getSingleOrNull();
|
||||
final dirtyWorkouts = await (db.select(db.workouts)
|
||||
..where((w) => w.isDirty.equals(true)))
|
||||
.get();
|
||||
|
||||
final dirtyWorkouts =
|
||||
await isar.workoutCollections.filter().isDirtyEqualTo(true).findAll();
|
||||
final validWorkouts =
|
||||
dirtyWorkouts.where((w) => w.cycleId.length > 5).toList();
|
||||
|
||||
if (dirtyUser == null && dirtyWorkouts.isEmpty) {
|
||||
debugPrint('✅ Nothing to push.');
|
||||
} else {
|
||||
final pushData = <String, dynamic>{
|
||||
'workouts': dirtyWorkouts.where((w) {
|
||||
return w.cycleId.length > 5;
|
||||
}).map((w) {
|
||||
return {
|
||||
'id': w.serverId,
|
||||
'local_id': w.id,
|
||||
'cycle_id': w.cycleId,
|
||||
'week': w.week,
|
||||
'day': w.day,
|
||||
'completed_at': w.completedAt?.toIso8601String(),
|
||||
'xp_earned': w.xpEarned,
|
||||
'notes': w.notes,
|
||||
'exercises': jsonDecode(w.exercisesJson),
|
||||
};
|
||||
}).toList(),
|
||||
'user_stats': dirtyUser != null
|
||||
? {
|
||||
'xp': dirtyUser.xp,
|
||||
'level': dirtyUser.level,
|
||||
'current_bodyweight': dirtyUser.currentBodyweight,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
final pushData = <String, dynamic>{
|
||||
'workouts': validWorkouts.map((w) {
|
||||
return {
|
||||
'id': w.serverId,
|
||||
'local_id': w.id,
|
||||
'cycle_id': w.cycleId,
|
||||
'week': w.week,
|
||||
'day': w.day,
|
||||
'completed_at': w.completedAt?.toIso8601String(),
|
||||
'xp_earned': w.xpEarned,
|
||||
'notes': w.notes,
|
||||
'exercises': w.exercises,
|
||||
};
|
||||
}).toList(),
|
||||
'user_stats': dirtyUser != null
|
||||
? {
|
||||
'xp': dirtyUser.xp,
|
||||
'level': dirtyUser.level,
|
||||
'current_bodyweight': dirtyUser.currentBodyweight,
|
||||
}
|
||||
: 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);
|
||||
|
||||
debugPrint(
|
||||
'☁️ Contacting server (Push: ${validWorkouts.length} workouts)...');
|
||||
|
||||
final response = await apiClient.sync(
|
||||
lastSyncTimestamp: lastSync ?? '',
|
||||
pushData: pushData,
|
||||
);
|
||||
|
||||
await db.transaction(() async {
|
||||
if (dirtyUser != null) {
|
||||
await (db.update(db.users)..where((u) => u.id.equals(dirtyUser.id)))
|
||||
.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)),
|
||||
);
|
||||
}
|
||||
|
||||
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
||||
if (response['pull_data'] != null) {
|
||||
if (response['pull_data']['cycles'] != null) {
|
||||
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();
|
||||
|
||||
if ((pushData['workouts'] as List).isNotEmpty ||
|
||||
pushData['user_stats'] != null) {
|
||||
debugPrint('📤 Pushing data...');
|
||||
final response = await apiClient.sync(
|
||||
lastSyncTimestamp: lastSync ?? '',
|
||||
pushData: pushData,
|
||||
);
|
||||
final tms = cJson['training_maxes'] as Map<String, dynamic>;
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
if (dirtyUser != null) {
|
||||
dirtyUser.isDirty = false;
|
||||
await isar.userCollections.put(dirtyUser);
|
||||
}
|
||||
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(),
|
||||
);
|
||||
|
||||
for (var w in dirtyWorkouts) {
|
||||
w.isDirty = false;
|
||||
await isar.workoutCollections.put(w);
|
||||
}
|
||||
|
||||
if (response['pull_data'] != null &&
|
||||
response['pull_data']['workouts'] != null) {
|
||||
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
||||
for (var wJson in pulledWorkouts) {
|
||||
final serverId = wJson['id'];
|
||||
var workout = await isar.workoutCollections
|
||||
.filter()
|
||||
.serverIdEqualTo(serverId)
|
||||
.findFirst();
|
||||
|
||||
workout ??= WorkoutCollection();
|
||||
|
||||
workout
|
||||
..serverId = serverId
|
||||
..cycleId = wJson['cycle_id']
|
||||
..userId = wJson['user_id']
|
||||
..week = wJson['week']
|
||||
..day = wJson['day']
|
||||
..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);
|
||||
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['server_timestamp'] != null) {
|
||||
await _storage.write(
|
||||
key: AppConstants.keyLastSync,
|
||||
value: response['server_timestamp'],
|
||||
);
|
||||
if (response['pull_data']['workouts'] != null) {
|
||||
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
||||
debugPrint(
|
||||
'📥 Pulled ${pulledWorkouts.length} workouts from server.');
|
||||
|
||||
for (var wJson in pulledWorkouts) {
|
||||
final serverId = wJson['id'] as String;
|
||||
final existing = await (db.select(db.workouts)
|
||||
..where((w) => w.serverId.equals(serverId)))
|
||||
.getSingleOrNull();
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
if (existing != null) {
|
||||
await (db.update(db.workouts)
|
||||
..where((w) => w.id.equals(existing.id)))
|
||||
.write(companion);
|
||||
} else {
|
||||
await db.into(db.workouts).insert(companion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (response['server_timestamp'] != null) {
|
||||
await _storage.write(
|
||||
key: AppConstants.keyLastSync,
|
||||
value: response['server_timestamp'],
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint('✅ Sync completed successfully');
|
||||
} catch (e) {
|
||||
} catch (e, stack) {
|
||||
debugPrint('❌ Sync failed: $e');
|
||||
debugPrint(stack.toString());
|
||||
} finally {
|
||||
_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:isar/isar.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../local/collections/cycle_collection.dart';
|
||||
import '../local/app_database.dart';
|
||||
import '../remote/api_client.dart';
|
||||
import '../../../../main.dart';
|
||||
import 'user_repository.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
import '../local/collections/workout_collection.dart';
|
||||
|
||||
final cycleRepositoryProvider = Provider<CycleRepository>((ref) {
|
||||
final isar = ref.watch(isarProvider);
|
||||
final db = ref.watch(appDatabaseProvider);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
return CycleRepository(isar: isar, apiClient: apiClient);
|
||||
return CycleRepository(db: db, apiClient: apiClient);
|
||||
});
|
||||
|
||||
class CycleRepository {
|
||||
final Isar isar;
|
||||
final AppDatabase db;
|
||||
final ApiClient apiClient;
|
||||
|
||||
CycleRepository({required this.isar, required this.apiClient});
|
||||
CycleRepository({required this.db, required this.apiClient});
|
||||
|
||||
Future<CycleCollection?> getCurrentCycle() async {
|
||||
return await isar.cycleCollections
|
||||
.filter()
|
||||
.isActiveEqualTo(true)
|
||||
.findFirst();
|
||||
return await (db.select(db.cycles)
|
||||
..where((c) => c.isActive.equals(true))
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
return await db.transaction(() async {
|
||||
final currentCycle = await getCurrentCycle();
|
||||
if (currentCycle != null) {
|
||||
currentCycle.isActive = false;
|
||||
currentCycle.endDate = DateTime.now();
|
||||
await saveCycle(currentCycle);
|
||||
final updateOld = CyclesCompanion(
|
||||
isActive: const Value(false),
|
||||
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();
|
||||
|
|
@ -50,34 +54,44 @@ class CycleRepository {
|
|||
.reduce((a, b) => a > b ? a : b) +
|
||||
1;
|
||||
|
||||
final userRepo = UserRepository(isar: isar, apiClient: ApiClient());
|
||||
final user = await userRepo.getLocalUser();
|
||||
|
||||
final user = await (db.select(db.users)..limit(1)).getSingleOrNull();
|
||||
if (user == null) {
|
||||
throw Exception('No user found for cycle creation');
|
||||
}
|
||||
|
||||
final newCycle = CycleCollection()
|
||||
..userId = user.serverId ?? user.id.toString()
|
||||
..cycleNumber = nextNumber
|
||||
..startDate = DateTime.now()
|
||||
..isActive = true
|
||||
..trainingMaxesJson = jsonEncode(trainingMaxes)
|
||||
..isDirty = true;
|
||||
final newCycleCompanion = CyclesCompanion(
|
||||
userId: Value(user.serverId ?? user.id.toString()),
|
||||
cycleNumber: Value(nextNumber),
|
||||
startDate: Value(DateTime.now()),
|
||||
isActive: const Value(true),
|
||||
trainingMaxes: Value(trainingMaxes),
|
||||
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 {
|
||||
final response = await apiClient.createCycle(trainingMaxes);
|
||||
newCycle.serverId = response['id'];
|
||||
newCycle.isDirty = false;
|
||||
await saveCycle(newCycle);
|
||||
} catch (e) {}
|
||||
await (db.update(db.cycles)..where((c) => c.id.equals(newId))).write(
|
||||
CyclesCompanion(
|
||||
serverId: Value(response['id']),
|
||||
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;
|
||||
} catch (e, stackTrace) {
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<CycleCollection> finishCycle() async {
|
||||
|
|
@ -87,24 +101,26 @@ class CycleRepository {
|
|||
}
|
||||
|
||||
final cycleIdRef = currentCycle.serverId ?? currentCycle.id.toString();
|
||||
final localCycleId = currentCycle.id.toString();
|
||||
|
||||
final completedMainWorkouts = await isar.workoutCollections
|
||||
.filter()
|
||||
.weekLessThan(4)
|
||||
.completedAtIsNotNull()
|
||||
.group((q) => q
|
||||
.cycleIdEqualTo(cycleIdRef)
|
||||
.or()
|
||||
.cycleIdEqualTo(currentCycle.id.toString()))
|
||||
.count();
|
||||
final workoutsQuery = db.select(db.workouts)
|
||||
..where((w) {
|
||||
final weekCheck = w.week.isSmallerThanValue(4);
|
||||
final completedCheck = w.completedAt.isNotNull();
|
||||
final cycleCheck =
|
||||
w.cycleId.equals(cycleIdRef) | w.cycleId.equals(localCycleId);
|
||||
return weekCheck & completedCheck & cycleCheck;
|
||||
});
|
||||
|
||||
final completedMainWorkouts = (await workoutsQuery.get()).length;
|
||||
|
||||
if (completedMainWorkouts < 9) {
|
||||
final missing = 9 - completedMainWorkouts;
|
||||
throw Exception(
|
||||
'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>{
|
||||
'squat': (currentTMs['squat'] as num?)?.toDouble() ?? 0.0,
|
||||
|
|
@ -112,23 +128,27 @@ class CycleRepository {
|
|||
'dip': (currentTMs['dip'] as num?)?.toDouble() ?? 0.0,
|
||||
};
|
||||
|
||||
final week3Workouts = await isar.workoutCollections
|
||||
.filter()
|
||||
.weekEqualTo(3)
|
||||
.group((q) => q
|
||||
.cycleIdEqualTo(cycleIdRef)
|
||||
.or()
|
||||
.cycleIdEqualTo(currentCycle.id.toString()))
|
||||
.findAll();
|
||||
final week3Workouts = await (db.select(db.workouts)
|
||||
..where((w) {
|
||||
final weekCheck = w.week.equals(3);
|
||||
final cycleCheck =
|
||||
w.cycleId.equals(cycleIdRef) | w.cycleId.equals(localCycleId);
|
||||
return weekCheck & cycleCheck;
|
||||
}))
|
||||
.get();
|
||||
|
||||
bool checkSuccess(String exerciseId) {
|
||||
for (var workout in week3Workouts) {
|
||||
try {
|
||||
final exercises = jsonDecode(workout.exercisesJson) as List;
|
||||
for (var ex in exercises) {
|
||||
final exercises = workout.exercises;
|
||||
|
||||
for (var exData in exercises) {
|
||||
final ex = exData as Map<String, dynamic>;
|
||||
|
||||
if (ex['exerciseId'] == exerciseId) {
|
||||
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) {
|
||||
final reps = s['repsActual'] as int? ?? 0;
|
||||
if (reps >= 1) {
|
||||
|
|
@ -170,33 +190,23 @@ class CycleRepository {
|
|||
try {
|
||||
await apiClient.finishCycle(currentCycle.serverId!);
|
||||
} catch (e) {
|
||||
// Fehler ignorieren, wird später gesynct
|
||||
// Fehler ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
return await createCycle(newTMs);
|
||||
}
|
||||
|
||||
Future<void> saveCycle(CycleCollection cycle) async {
|
||||
cycle.updatedAt = DateTime.now();
|
||||
await isar.writeTxn(() async {
|
||||
await isar.cycleCollections.put(cycle);
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, double> getCurrentTrainingMaxes() {
|
||||
final cycle =
|
||||
isar.cycleCollections.filter().isActiveEqualTo(true).findFirstSync();
|
||||
|
||||
Future<Map<String, double>> getCurrentTrainingMaxesAsync() async {
|
||||
final cycle = await getCurrentCycle();
|
||||
if (cycle != null) {
|
||||
final tms = jsonDecode(cycle.trainingMaxesJson);
|
||||
final tms = cycle.trainingMaxes;
|
||||
return {
|
||||
'squat': (tms['squat'] as num?)?.toDouble() ?? 0.0,
|
||||
'pullup': (tms['pullup'] as num?)?.toDouble() ?? 0.0,
|
||||
'dip': (tms['dip'] as num?)?.toDouble() ?? 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 '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/collections/user_collection.dart';
|
||||
import '../local/collections/workout_collection.dart';
|
||||
import '../local/app_database.dart';
|
||||
import '../remote/api_client.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
|
||||
final userRepositoryProvider = Provider<UserRepository>((ref) {
|
||||
final isar = ref.watch(isarProvider);
|
||||
final db = ref.watch(appDatabaseProvider);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
return UserRepository(isar: isar, apiClient: apiClient);
|
||||
return UserRepository(db: db, apiClient: apiClient);
|
||||
});
|
||||
|
||||
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());
|
||||
|
||||
class UserRepository {
|
||||
final Isar isar;
|
||||
final AppDatabase db;
|
||||
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 {
|
||||
return await isar.userCollections.where().findFirst();
|
||||
return await (db.select(db.users)..limit(1)).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> saveLocalUser(UserCollection user) async {
|
||||
user.updatedAt = DateTime.now();
|
||||
await isar.writeTxn(() async {
|
||||
await isar.userCollections.put(user);
|
||||
});
|
||||
final companion = user.toCompanion(true).copyWith(
|
||||
updatedAt: Value(DateTime.now()),
|
||||
);
|
||||
await db.into(db.users).insertOnConflictUpdate(companion);
|
||||
}
|
||||
|
||||
Future<void> updateXP(int xpToAdd) async {
|
||||
final user = await getLocalUser();
|
||||
if (user != null) {
|
||||
user.xp += xpToAdd;
|
||||
user.isDirty = true;
|
||||
await saveLocalUser(user);
|
||||
final newXp = user.xp + xpToAdd;
|
||||
final companion = UsersCompanion(
|
||||
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 {
|
||||
final user = await getLocalUser();
|
||||
if (user != null) {
|
||||
user.level = newLevel;
|
||||
user.isDirty = true;
|
||||
await saveLocalUser(user);
|
||||
final companion = UsersCompanion(
|
||||
level: Value(newLevel),
|
||||
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 {
|
||||
final user = await getLocalUser();
|
||||
if (user != null) {
|
||||
user.currentBodyweight = bodyweight;
|
||||
user.isDirty = true;
|
||||
await saveLocalUser(user);
|
||||
final companion = UsersCompanion(
|
||||
currentBodyweight: Value(bodyweight),
|
||||
isDirty: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
);
|
||||
await (db.update(db.users)..where((u) => u.id.equals(user.id)))
|
||||
.write(companion);
|
||||
|
||||
try {
|
||||
await apiClient.updateBodyweight(bodyweight);
|
||||
|
|
@ -67,9 +81,13 @@ class UserRepository {
|
|||
Future<void> updateInventory(Map<String, dynamic> inventory) async {
|
||||
final user = await getLocalUser();
|
||||
if (user != null) {
|
||||
user.inventorySettingsJson = jsonEncode(inventory);
|
||||
user.isDirty = true;
|
||||
await saveLocalUser(user);
|
||||
final companion = UsersCompanion(
|
||||
inventorySettings: Value(inventory),
|
||||
isDirty: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
);
|
||||
await (db.update(db.users)..where((u) => u.id.equals(user.id)))
|
||||
.write(companion);
|
||||
|
||||
try {
|
||||
await apiClient.updateInventory(inventory);
|
||||
|
|
@ -79,21 +97,7 @@ class UserRepository {
|
|||
|
||||
Future<UserCollection> login(String email, String password) async {
|
||||
final response = await apiClient.login(email, password);
|
||||
|
||||
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;
|
||||
return _saveUserFromApi(response['record']);
|
||||
}
|
||||
|
||||
Future<UserCollection> register({
|
||||
|
|
@ -111,48 +115,57 @@ class UserRepository {
|
|||
);
|
||||
|
||||
final record = response['record'] ?? response;
|
||||
|
||||
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);
|
||||
final user = await _saveUserFromApi(record);
|
||||
|
||||
try {
|
||||
await apiClient.login(email, password);
|
||||
} catch (e) {}
|
||||
|
||||
return user;
|
||||
} catch (e, stackTrace) {
|
||||
} catch (e) {
|
||||
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 {
|
||||
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() {
|
||||
final user = isar.userCollections.where().findFirstSync();
|
||||
if (user?.inventorySettingsJson != null) {
|
||||
return jsonDecode(user!.inventorySettingsJson!);
|
||||
Future<Map<String, dynamic>> getInventorySettingsAsync() async {
|
||||
final user = await getLocalUser();
|
||||
if (user?.inventorySettings != null) {
|
||||
return user!.inventorySettings!;
|
||||
}
|
||||
return {
|
||||
'bar_weight': 20.0,
|
||||
|
|
@ -161,14 +174,14 @@ class UserRepository {
|
|||
};
|
||||
}
|
||||
|
||||
List<double> getAvailablePlates() {
|
||||
final inventory = getInventorySettings();
|
||||
Future<List<double>> getAvailablePlates() async {
|
||||
final inventory = await getInventorySettingsAsync();
|
||||
final plates = inventory['plates'] as List?;
|
||||
return plates?.map((e) => (e as num).toDouble()).toList() ?? [];
|
||||
}
|
||||
|
||||
double getBarWeight() {
|
||||
final inventory = getInventorySettings();
|
||||
Future<double> getBarWeight() async {
|
||||
final inventory = await getInventorySettingsAsync();
|
||||
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.");
|
||||
}
|
||||
|
||||
user.xp = 0;
|
||||
user.level = 1;
|
||||
final companion = UsersCompanion(
|
||||
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 isar.writeTxn(() async {
|
||||
await isar.userCollections.put(user);
|
||||
|
||||
await isar.cycleCollections.clear();
|
||||
await isar.workoutCollections.clear();
|
||||
});
|
||||
await db.delete(db.cycles).go();
|
||||
await db.delete(db.workouts).go();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,44 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../local/collections/workout_collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import '../local/app_database.dart';
|
||||
import '../remote/api_client.dart';
|
||||
import '../../../../main.dart';
|
||||
import 'user_repository.dart';
|
||||
|
||||
final workoutRepositoryProvider = Provider<WorkoutRepository>((ref) {
|
||||
final isar = ref.watch(isarProvider);
|
||||
final db = ref.watch(appDatabaseProvider);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
return WorkoutRepository(isar: isar, apiClient: apiClient);
|
||||
return WorkoutRepository(db: db, apiClient: apiClient);
|
||||
});
|
||||
|
||||
class WorkoutRepository {
|
||||
final Isar isar;
|
||||
final AppDatabase db;
|
||||
final ApiClient apiClient;
|
||||
|
||||
WorkoutRepository({required this.isar, required this.apiClient});
|
||||
WorkoutRepository({required this.db, required this.apiClient});
|
||||
|
||||
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 {
|
||||
return await isar.workoutCollections
|
||||
.filter()
|
||||
.cycleIdEqualTo(cycleId)
|
||||
.findAll();
|
||||
return await (db.select(db.workouts)
|
||||
..where((w) => w.cycleId.equals(cycleId)))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<WorkoutCollection>> getCompletedWorkouts(String userId) async {
|
||||
return await isar.workoutCollections
|
||||
.filter()
|
||||
.userIdEqualTo(userId)
|
||||
.completedAtIsNotNull()
|
||||
.findAll();
|
||||
return await (db.select(db.workouts)
|
||||
..where((w) => w.userId.equals(userId) & w.completedAt.isNotNull()))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<void> saveWorkout(WorkoutCollection workout) async {
|
||||
workout.updatedAt = DateTime.now();
|
||||
workout.isDirty = true;
|
||||
await isar.writeTxn(() async {
|
||||
await isar.workoutCollections.put(workout);
|
||||
});
|
||||
final companion = workout.toCompanion(true).copyWith(
|
||||
updatedAt: Value(DateTime.now()),
|
||||
isDirty: const Value(true),
|
||||
);
|
||||
await db.into(db.workouts).insertOnConflictUpdate(companion);
|
||||
}
|
||||
|
||||
Future<WorkoutCollection> createWorkout({
|
||||
|
|
@ -51,27 +46,42 @@ class WorkoutRepository {
|
|||
required String cycleId,
|
||||
required int week,
|
||||
required int day,
|
||||
required String exercisesJson,
|
||||
required List<dynamic> exercises,
|
||||
}) async {
|
||||
final workout = WorkoutCollection()
|
||||
..userId = userId
|
||||
..cycleId = cycleId
|
||||
..week = week
|
||||
..day = day
|
||||
..exercisesJson = exercisesJson
|
||||
..scheduledDate = DateTime.now();
|
||||
final companion = WorkoutsCompanion(
|
||||
userId: Value(userId),
|
||||
cycleId: Value(cycleId),
|
||||
week: Value(week),
|
||||
day: Value(day),
|
||||
exercises: Value(exercises),
|
||||
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);
|
||||
return workout;
|
||||
final id = await db.into(db.workouts).insert(companion);
|
||||
return await (db.select(db.workouts)..where((w) => w.id.equals(id)))
|
||||
.getSingle();
|
||||
}
|
||||
|
||||
Future<void> completeWorkout(
|
||||
WorkoutCollection workout, {
|
||||
required int xpEarned,
|
||||
}) async {
|
||||
workout.completedAt = DateTime.now();
|
||||
workout.xpEarned = xpEarned;
|
||||
await saveWorkout(workout);
|
||||
final companion = WorkoutsCompanion(
|
||||
id: Value(workout.id),
|
||||
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({
|
||||
|
|
@ -80,16 +90,17 @@ class WorkoutRepository {
|
|||
required int week,
|
||||
required int day,
|
||||
}) async {
|
||||
return await isar.workoutCollections
|
||||
.filter()
|
||||
.weekEqualTo(week)
|
||||
.dayEqualTo(day)
|
||||
.group((q) {
|
||||
var query = q.cycleIdEqualTo(cycleId);
|
||||
if (localCycleId != null) {
|
||||
query = query.or().cycleIdEqualTo(localCycleId);
|
||||
}
|
||||
return query;
|
||||
}).findFirst();
|
||||
return await (db.select(db.workouts)
|
||||
..where((w) {
|
||||
final weekDayCheck = w.week.equals(week) & w.day.equals(day);
|
||||
|
||||
Expression<bool> cycleCheck = w.cycleId.equals(cycleId);
|
||||
if (localCycleId != null) {
|
||||
cycleCheck = cycleCheck | w.cycleId.equals(localCycleId);
|
||||
}
|
||||
|
||||
return weekDayCheck & cycleCheck;
|
||||
}))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
132
pubspec.lock
132
pubspec.lock
|
|
@ -5,26 +5,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "61.0.0"
|
||||
version: "67.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
version: "6.4.1"
|
||||
analyzer_plugin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer_plugin
|
||||
sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d
|
||||
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.2"
|
||||
version: "0.11.3"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -145,6 +145,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -153,6 +161,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -217,14 +233,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
dartx:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dartx
|
||||
sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -241,6 +249,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -480,30 +512,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -744,6 +752,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -933,6 +949,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -997,14 +1037,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: time
|
||||
sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1125,14 +1157,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
xxh3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xxh3
|
||||
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ dependencies:
|
|||
riverpod_annotation: ^2.3.5
|
||||
|
||||
# Local Database
|
||||
isar: ^3.1.0+1
|
||||
isar_flutter_libs: ^3.1.0+1
|
||||
drift: ^2.16.0
|
||||
drift_flutter: ^0.1.0
|
||||
sqlite3_flutter_libs: ^0.5.20
|
||||
path_provider: ^2.1.3
|
||||
|
||||
# Networking
|
||||
|
|
@ -52,7 +53,7 @@ dev_dependencies:
|
|||
# Code Generation
|
||||
build_runner: ^2.4.9
|
||||
riverpod_generator: ^2.4.0
|
||||
isar_generator: ^3.1.0+1
|
||||
drift_dev: ^2.16.0
|
||||
freezed: ^2.5.2
|
||||
json_serializable: ^6.8.0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue