feat: add basic quest engine
This commit is contained in:
parent
ee89f327bd
commit
311d764a4d
23 changed files with 5056 additions and 1449 deletions
BIN
assets/images/backgrounds/commercial_gym.png
Normal file
BIN
assets/images/backgrounds/commercial_gym.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/images/backgrounds/olympic_gym.png
Normal file
BIN
assets/images/backgrounds/olympic_gym.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/images/backgrounds/street_park_night.png
Normal file
BIN
assets/images/backgrounds/street_park_night.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
|
|
@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import '../../features/authentication/presentation/screens/login_screen.dart';
|
import '../../features/authentication/presentation/screens/login_screen.dart';
|
||||||
import '../../features/authentication/presentation/screens/profile_screen.dart';
|
import '../../features/authentication/presentation/screens/profile_screen.dart';
|
||||||
import '../../features/authentication/presentation/screens/register_screen.dart';
|
import '../../features/authentication/presentation/screens/register_screen.dart';
|
||||||
|
import '../../features/gamification/presentation/screens/quest_log.dart';
|
||||||
import '../../features/onboarding/presentation/screens/avatar_setup_screen.dart';
|
import '../../features/onboarding/presentation/screens/avatar_setup_screen.dart';
|
||||||
import '../../features/onboarding/presentation/screens/welcome_screen.dart';
|
import '../../features/onboarding/presentation/screens/welcome_screen.dart';
|
||||||
import '../../features/onboarding/presentation/screens/bodyweight_input_screen.dart';
|
import '../../features/onboarding/presentation/screens/bodyweight_input_screen.dart';
|
||||||
|
|
@ -113,6 +114,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||||
name: 'codex',
|
name: 'codex',
|
||||||
builder: (context, state) => const CodexScreen(),
|
builder: (context, state) => const CodexScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/quests',
|
||||||
|
name: 'quests',
|
||||||
|
builder: (context, state) => const QuestLogScreen(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ 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 '../../../gamification/domain/entities/item_catalog.dart';
|
||||||
|
|
||||||
class ProfileScreen extends ConsumerStatefulWidget {
|
class ProfileScreen extends ConsumerStatefulWidget {
|
||||||
const ProfileScreen({super.key});
|
const ProfileScreen({super.key});
|
||||||
|
|
@ -141,6 +142,146 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showBackgroundSelector() {
|
||||||
|
final currentLevel = _user?.level ?? 1;
|
||||||
|
final currentConfig = _user?.avatarConfig != null
|
||||||
|
? AvatarConfig.fromJson(_user!.avatarConfig!)
|
||||||
|
: const AvatarConfig();
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: AppTheme.surfaceColor,
|
||||||
|
builder: (context) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text('Select Scenery',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
itemCount: ItemCatalog.backgrounds.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = ItemCatalog.backgrounds[index];
|
||||||
|
final isUnlocked = currentLevel >= item.unlockLevel;
|
||||||
|
final isSelected = currentConfig.selectedBackground == item.id;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: isUnlocked
|
||||||
|
? () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
// Update Config
|
||||||
|
final newConfig = AvatarConfig(
|
||||||
|
gender: currentConfig.gender,
|
||||||
|
variant: currentConfig.variant,
|
||||||
|
selectedBackground: item.id, // Hintergrund setzen
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedUser = _user!.copyWith(
|
||||||
|
avatarConfig: Value(newConfig.toJson()),
|
||||||
|
isDirty: true,
|
||||||
|
);
|
||||||
|
_user = updatedUser;
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(userRepositoryProvider)
|
||||||
|
.saveLocalUser(_user!);
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
// Save to DB
|
||||||
|
// await ref
|
||||||
|
// .read(userRepositoryProvider)
|
||||||
|
// .updateAvatarConfig(newConfig.toJson());
|
||||||
|
// await _loadUser();
|
||||||
|
// setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Container(
|
||||||
|
width: 140,
|
||||||
|
margin: const EdgeInsets.only(right: 12, bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
isSelected ? AppTheme.primaryColor : Colors.white12,
|
||||||
|
width: isSelected ? 3 : 1,
|
||||||
|
),
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(item.assetPath),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
colorFilter: isUnlocked
|
||||||
|
? null
|
||||||
|
: const ColorFilter.mode(
|
||||||
|
Colors.black87, BlendMode.darken),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (!isUnlocked)
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.lock, color: Colors.white54),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Lvl ${item.unlockLevel}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
blurRadius: 4, color: Colors.black)
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
const Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Icon(Icons.check_circle,
|
||||||
|
color: AppTheme.primaryColor),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
bottom: Radius.circular(10)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
item.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _confirmDangerAction(
|
void _confirmDangerAction(
|
||||||
String title, String content, VoidCallback onConfirm) {
|
String title, String content, VoidCallback onConfirm) {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
|
@ -270,6 +411,15 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: _showBackgroundSelector,
|
||||||
|
icon: const Icon(Icons.landscape),
|
||||||
|
label: const Text('CHANGE SCENERY'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
Text('Physical Stats',
|
Text('Physical Stats',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,13 @@ import '../../../../shared/domain/logic/wendler_calculator.dart';
|
||||||
import '../../../../shared/domain/entities/exercise.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/domain/entities/item_catalog.dart';
|
||||||
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
|
||||||
|
import '../../../gamification/presentation/widgets/quest_board.dart';
|
||||||
import '../widgets/xp_bar_widget.dart';
|
import '../widgets/xp_bar_widget.dart';
|
||||||
import '../widgets/level_display.dart';
|
import '../widgets/level_display.dart';
|
||||||
import '../widgets/start_raid_button.dart';
|
import '../widgets/start_raid_button.dart';
|
||||||
|
import '../../../gamification/application/quest_service.dart';
|
||||||
|
|
||||||
class HubScreen extends ConsumerStatefulWidget {
|
class HubScreen extends ConsumerStatefulWidget {
|
||||||
const HubScreen({super.key});
|
const HubScreen({super.key});
|
||||||
|
|
@ -40,8 +43,9 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
_runSync();
|
await _runSync();
|
||||||
|
await ref.read(questServiceProvider).checkAndGenerateQuests();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,6 +209,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
final avatarConfig = user?.avatarConfig != null
|
final avatarConfig = user?.avatarConfig != null
|
||||||
? AvatarConfig.fromJson(user!.avatarConfig!)
|
? AvatarConfig.fromJson(user!.avatarConfig!)
|
||||||
: const AvatarConfig();
|
: const AvatarConfig();
|
||||||
|
final bgItem =
|
||||||
|
ItemCatalog.getBackground(avatarConfig.selectedBackground);
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
|
@ -223,10 +229,18 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
AssetPaths.bgStreetParkDay,
|
bgItem.assetPath,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
// Key hinzufügen, damit Flutter einen sanften Übergang animieren kann (optional)
|
||||||
|
key: ValueKey(bgItem.assetPath),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Positioned.fill(
|
||||||
|
// child: Image.asset(
|
||||||
|
// AssetPaths.bgStreetParkDay,
|
||||||
|
// fit: BoxFit.cover,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -289,6 +303,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
||||||
nextLevelXP: nextLevelXP,
|
nextLevelXP: nextLevelXP,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const QuestBoardWidget(),
|
||||||
const Spacer(flex: 2),
|
const Spacer(flex: 2),
|
||||||
if (cycle != null)
|
if (cycle != null)
|
||||||
Padding(
|
Padding(
|
||||||
|
|
|
||||||
179
lib/src/features/gamification/application/quest_service.dart
Normal file
179
lib/src/features/gamification/application/quest_service.dart
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
import '../../../shared/data/local/app_database.dart';
|
||||||
|
import '../../../shared/data/repositories/user_repository.dart';
|
||||||
|
import '../../../shared/data/repositories/workout_repository.dart';
|
||||||
|
import '../data/repositories/quest_repository.dart';
|
||||||
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
|
||||||
|
enum QuestTrigger {
|
||||||
|
workoutComplete,
|
||||||
|
volume,
|
||||||
|
repCount,
|
||||||
|
inventoryChange,
|
||||||
|
}
|
||||||
|
|
||||||
|
final questServiceProvider = Provider<QuestService>((ref) {
|
||||||
|
final questRepo = ref.watch(questRepositoryProvider);
|
||||||
|
final userRepo = ref.watch(userRepositoryProvider);
|
||||||
|
return QuestService(ref: ref, questRepo: questRepo, userRepo: userRepo);
|
||||||
|
});
|
||||||
|
|
||||||
|
class QuestService {
|
||||||
|
final Ref ref;
|
||||||
|
final QuestRepository questRepo;
|
||||||
|
final UserRepository userRepo;
|
||||||
|
|
||||||
|
QuestService({
|
||||||
|
required this.ref,
|
||||||
|
required this.questRepo,
|
||||||
|
required this.userRepo,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> checkAndGenerateQuests() async {
|
||||||
|
await _cleanupExpired();
|
||||||
|
await _generateDailiesIfNeeded();
|
||||||
|
await _ensureStoryQuests();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cleanupExpired() async {
|
||||||
|
await questRepo.cleanupExpiredQuests();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _generateDailiesIfNeeded() async {
|
||||||
|
final activeDailies = await questRepo.getActiveQuests();
|
||||||
|
final hasDailies = activeDailies.any((q) => q.type == 'daily');
|
||||||
|
|
||||||
|
if (!hasDailies) {
|
||||||
|
debugPrint('🎲 Generating new Daily Quests...');
|
||||||
|
final random = Random();
|
||||||
|
final newQuests = <QuestCollection>[];
|
||||||
|
|
||||||
|
final pool = List<_QuestTemplate>.from(_dailyQuestPool)..shuffle();
|
||||||
|
final selected = pool.take(3);
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final endOfDay = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||||
|
|
||||||
|
for (var template in selected) {
|
||||||
|
newQuests.add(QuestCollection(
|
||||||
|
id: 'daily_${now.millisecondsSinceEpoch}_${random.nextInt(1000)}',
|
||||||
|
type: 'daily',
|
||||||
|
title: template.title,
|
||||||
|
description: template.description,
|
||||||
|
targetValue: template.target,
|
||||||
|
currentValue: 0,
|
||||||
|
rewardXP: template.xp,
|
||||||
|
rewardItem: template.itemId,
|
||||||
|
isCompleted: false,
|
||||||
|
isClaimed: false,
|
||||||
|
expiresAt: endOfDay,
|
||||||
|
createdAt: now,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var q in newQuests) {
|
||||||
|
await questRepo.createQuest(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _ensureStoryQuests() async {
|
||||||
|
final activeQuests = await questRepo.getActiveQuests();
|
||||||
|
|
||||||
|
// Helper: Prüft ob Quest-ID schon existiert (aktiv oder erledigt)
|
||||||
|
// Hinweis: getActiveQuests liefert aktuell alle nicht-abgelaufenen.
|
||||||
|
// Für Story Quests (die nie ablaufen) reicht das.
|
||||||
|
bool hasQuest(String id) => activeQuests.any((q) => q.id == id);
|
||||||
|
|
||||||
|
if (!hasQuest('story_initiate')) {
|
||||||
|
await questRepo.createQuest(QuestCollection(
|
||||||
|
id: 'story_initiate',
|
||||||
|
type: 'story',
|
||||||
|
title: 'The Awakening',
|
||||||
|
description: 'Complete your first workout to prove your worth.',
|
||||||
|
targetValue: 1,
|
||||||
|
currentValue: 0,
|
||||||
|
rewardXP: 100,
|
||||||
|
isCompleted: false,
|
||||||
|
isClaimed: false,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasQuest('story_inventory')) {
|
||||||
|
await questRepo.createQuest(QuestCollection(
|
||||||
|
id: 'story_inventory',
|
||||||
|
type: 'story',
|
||||||
|
title: 'Armory Master',
|
||||||
|
description: 'Setup your equipment inventory.',
|
||||||
|
targetValue: 1,
|
||||||
|
currentValue: 0,
|
||||||
|
rewardXP: 50,
|
||||||
|
isCompleted: false,
|
||||||
|
isClaimed: false,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
));
|
||||||
|
final inventory = await userRepo.getInventorySettingsAsync();
|
||||||
|
if ((inventory['plates'] as List).isNotEmpty) {
|
||||||
|
await questRepo.updateProgress('story_inventory', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reportEvent(QuestTrigger trigger, {dynamic data}) async {
|
||||||
|
final activeQuests = await questRepo.getActiveQuests();
|
||||||
|
|
||||||
|
for (var quest in activeQuests) {
|
||||||
|
if (quest.isCompleted) continue;
|
||||||
|
|
||||||
|
if (quest.id == 'story_initiate' &&
|
||||||
|
trigger == QuestTrigger.workoutComplete) {
|
||||||
|
await questRepo.updateProgress(quest.id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quest.title == 'Volume Eater' && trigger == QuestTrigger.volume) {
|
||||||
|
final volume = data as int; // kg
|
||||||
|
await questRepo.updateProgress(quest.id, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quest.title == 'Workout Warrior' &&
|
||||||
|
trigger == QuestTrigger.workoutComplete) {
|
||||||
|
await questRepo.updateProgress(quest.id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quest.title == 'Rep Collector' && trigger == QuestTrigger.repCount) {
|
||||||
|
final reps = data as int;
|
||||||
|
await questRepo.updateProgress(quest.id, reps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- QUEST DEFINITIONS ---
|
||||||
|
|
||||||
|
class _QuestTemplate {
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final int target;
|
||||||
|
final int xp;
|
||||||
|
final String? itemId;
|
||||||
|
|
||||||
|
const _QuestTemplate(this.title, this.description, this.target, this.xp,
|
||||||
|
[this.itemId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const List<_QuestTemplate> _dailyQuestPool = [
|
||||||
|
_QuestTemplate(
|
||||||
|
'Volume Eater', 'Move a total of 500kg in a single day.', 500, 100),
|
||||||
|
_QuestTemplate('Workout Warrior', 'Complete 1 Workout today.', 1, 50),
|
||||||
|
_QuestTemplate('Rep Collector',
|
||||||
|
'Perform 50 total repetitions across all exercises.', 50, 75),
|
||||||
|
_QuestTemplate('Early Bird', 'Start a workout before noon.', 1,
|
||||||
|
50), // Logik müsste Zeit prüfen
|
||||||
|
_QuestTemplate(
|
||||||
|
'Iron Discipline', 'Log your bodyweight in the profile.', 1, 25),
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
|
import '../../../../../main.dart';
|
||||||
|
|
||||||
|
final questRepositoryProvider = Provider<QuestRepository>((ref) {
|
||||||
|
final db = ref.watch(appDatabaseProvider);
|
||||||
|
return QuestRepository(db: db);
|
||||||
|
});
|
||||||
|
|
||||||
|
class QuestRepository {
|
||||||
|
final AppDatabase db;
|
||||||
|
|
||||||
|
QuestRepository({required this.db});
|
||||||
|
|
||||||
|
Future<List<QuestCollection>> getActiveQuests() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return await (db.select(db.quests)
|
||||||
|
..where((q) {
|
||||||
|
final notExpired =
|
||||||
|
q.expiresAt.isNull() | q.expiresAt.isBiggerThanValue(now);
|
||||||
|
return notExpired;
|
||||||
|
})
|
||||||
|
..orderBy([
|
||||||
|
(q) =>
|
||||||
|
OrderingTerm(expression: q.isCompleted, mode: OrderingMode.asc)
|
||||||
|
])) // Offene zuerst
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createQuest(QuestCollection quest) async {
|
||||||
|
await db.into(db.quests).insertOnConflictUpdate(QuestsCompanion(
|
||||||
|
id: Value(quest.id),
|
||||||
|
type: Value(quest.type),
|
||||||
|
title: Value(quest.title),
|
||||||
|
description: Value(quest.description),
|
||||||
|
targetValue: Value(quest.targetValue),
|
||||||
|
currentValue: Value(quest.currentValue),
|
||||||
|
rewardXP: Value(quest.rewardXP),
|
||||||
|
rewardItem: Value(quest.rewardItem),
|
||||||
|
expiresAt: Value(quest.expiresAt),
|
||||||
|
createdAt: Value(DateTime.now()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateProgress(String questId, int addValue) async {
|
||||||
|
final quest = await (db.select(db.quests)
|
||||||
|
..where((q) => q.id.equals(questId)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (quest != null && !quest.isCompleted) {
|
||||||
|
final newValue = quest.currentValue + addValue;
|
||||||
|
final isComplete = newValue >= quest.targetValue;
|
||||||
|
|
||||||
|
await (db.update(db.quests)..where((q) => q.id.equals(questId)))
|
||||||
|
.write(QuestsCompanion(
|
||||||
|
currentValue: Value(newValue),
|
||||||
|
isCompleted: Value(isComplete),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> claimQuest(String questId) async {
|
||||||
|
await (db.update(db.quests)..where((q) => q.id.equals(questId))).write(
|
||||||
|
const QuestsCompanion(isClaimed: Value(true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cleanupExpiredQuests() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
await (db.delete(db.quests)
|
||||||
|
..where((q) => q.expiresAt.isSmallerThanValue(now)))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<QuestCollection>> watchQuests() {
|
||||||
|
return (db.select(db.quests)
|
||||||
|
..orderBy([
|
||||||
|
(q) =>
|
||||||
|
OrderingTerm(expression: q.isCompleted, mode: OrderingMode.desc)
|
||||||
|
]))
|
||||||
|
.watch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,50 @@
|
||||||
|
// import '../../../../core/constants/asset_paths.dart';
|
||||||
|
|
||||||
|
// class AvatarConfig {
|
||||||
|
// final String gender; // 'male' or 'female'
|
||||||
|
// final int variant; // 1 to 8
|
||||||
|
// final String selectedBackground;
|
||||||
|
|
||||||
|
// const AvatarConfig({
|
||||||
|
// this.gender = 'male',
|
||||||
|
// this.variant = 1,
|
||||||
|
// this.selectedBackground = 'bg_street_day',
|
||||||
|
// });
|
||||||
|
|
||||||
|
// factory AvatarConfig.fromJson(Map<String, dynamic> json) {
|
||||||
|
// return AvatarConfig(
|
||||||
|
// gender: json['gender'] ?? 'male',
|
||||||
|
// variant: json['variant'] ?? 1,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Map<String, dynamic> toJson() {
|
||||||
|
// return {
|
||||||
|
// 'gender': gender,
|
||||||
|
// 'variant': variant,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// String get assetPath => AssetPaths.getAvatarPath(gender, variant);
|
||||||
|
// }
|
||||||
import '../../../../core/constants/asset_paths.dart';
|
import '../../../../core/constants/asset_paths.dart';
|
||||||
|
|
||||||
class AvatarConfig {
|
class AvatarConfig {
|
||||||
final String gender; // 'male' or 'female'
|
final String gender;
|
||||||
final int variant; // 1 to 8
|
final int variant;
|
||||||
|
final String selectedBackground; // NEU
|
||||||
|
|
||||||
const AvatarConfig({
|
const AvatarConfig({
|
||||||
this.gender = 'male',
|
this.gender = 'male',
|
||||||
this.variant = 1,
|
this.variant = 1,
|
||||||
|
this.selectedBackground = 'bg_street_day', // Default
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AvatarConfig.fromJson(Map<String, dynamic> json) {
|
factory AvatarConfig.fromJson(Map<String, dynamic> json) {
|
||||||
return AvatarConfig(
|
return AvatarConfig(
|
||||||
gender: json['gender'] ?? 'male',
|
gender: json['gender'] ?? 'male',
|
||||||
variant: json['variant'] ?? 1,
|
variant: json['variant'] ?? 1,
|
||||||
|
selectedBackground: json['selected_background'] ?? 'bg_street_day', // NEU
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,6 +52,7 @@ class AvatarConfig {
|
||||||
return {
|
return {
|
||||||
'gender': gender,
|
'gender': gender,
|
||||||
'variant': variant,
|
'variant': variant,
|
||||||
|
'selected_background': selectedBackground, // NEU
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import '../../../../core/constants/asset_paths.dart';
|
||||||
|
|
||||||
|
enum ItemCategory { background, badge }
|
||||||
|
|
||||||
|
class UnlockableItem {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final String assetPath;
|
||||||
|
final ItemCategory category;
|
||||||
|
final int unlockLevel; // 0 = Start
|
||||||
|
final String? unlockQuestId; // Optional für später
|
||||||
|
|
||||||
|
const UnlockableItem({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.assetPath,
|
||||||
|
required this.category,
|
||||||
|
this.unlockLevel = 0,
|
||||||
|
this.unlockQuestId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ItemCatalog {
|
||||||
|
// Alle verfügbaren Hintergründe
|
||||||
|
static const List<UnlockableItem> backgrounds = [
|
||||||
|
UnlockableItem(
|
||||||
|
id: 'bg_street_day',
|
||||||
|
name: 'Street Park (Day)',
|
||||||
|
description: 'Where every journey begins.',
|
||||||
|
assetPath: AssetPaths.bgStreetParkDay,
|
||||||
|
category: ItemCategory.background,
|
||||||
|
unlockLevel: 0, // Start
|
||||||
|
),
|
||||||
|
UnlockableItem(
|
||||||
|
id: 'bg_street_night',
|
||||||
|
name: 'Street Park (Night)',
|
||||||
|
description: 'For those who grind while others sleep.',
|
||||||
|
assetPath: AssetPaths.bgStreetParkNight,
|
||||||
|
category: ItemCategory.background,
|
||||||
|
unlockLevel: 5,
|
||||||
|
),
|
||||||
|
UnlockableItem(
|
||||||
|
id: 'bg_commercial',
|
||||||
|
name: 'Commercial Gym',
|
||||||
|
description: 'Clean equipment, AC, and mirrors everywhere.',
|
||||||
|
assetPath: AssetPaths.bgCommercialGym,
|
||||||
|
category: ItemCategory.background,
|
||||||
|
unlockLevel: 10,
|
||||||
|
),
|
||||||
|
UnlockableItem(
|
||||||
|
id: 'bg_underground',
|
||||||
|
name: 'Underground Dojo',
|
||||||
|
description: 'No rules. Just heavy iron and concrete.',
|
||||||
|
assetPath: AssetPaths.bgUndergroundGym,
|
||||||
|
category: ItemCategory.background,
|
||||||
|
unlockLevel: 20,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
static UnlockableItem getBackground(String id) {
|
||||||
|
return backgrounds.firstWhere(
|
||||||
|
(b) => b.id == id,
|
||||||
|
orElse: () => backgrounds.first, // Fallback auf Start
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
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 '../../data/repositories/quest_repository.dart';
|
||||||
|
import '../../../../shared/data/local/app_database.dart'; // Für QuestCollection Typ
|
||||||
|
import '../widgets/quest_item.dart';
|
||||||
|
|
||||||
|
class QuestLogScreen extends ConsumerWidget {
|
||||||
|
const QuestLogScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final questRepo = ref.watch(questRepositoryProvider);
|
||||||
|
|
||||||
|
return DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Quest Log'),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => context.go('/hub'),
|
||||||
|
),
|
||||||
|
bottom: const TabBar(
|
||||||
|
indicatorColor: AppTheme.primaryColor,
|
||||||
|
labelColor: AppTheme.primaryColor,
|
||||||
|
unselectedLabelColor: Colors.grey,
|
||||||
|
tabs: [
|
||||||
|
Tab(text: 'DAILIES'),
|
||||||
|
Tab(text: 'JOURNEY'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: StreamBuilder<List<QuestCollection>>(
|
||||||
|
stream: questRepo.watchQuests(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('Error: ${snapshot.error}'));
|
||||||
|
}
|
||||||
|
|
||||||
|
final allQuests = snapshot.data ?? [];
|
||||||
|
|
||||||
|
// Filtern
|
||||||
|
final dailies = allQuests.where((q) => q.type == 'daily').toList();
|
||||||
|
final story = allQuests.where((q) => q.type != 'daily').toList();
|
||||||
|
|
||||||
|
return TabBarView(
|
||||||
|
children: [
|
||||||
|
_QuestList(quests: dailies, emptyMessage: 'No daily quests available.\nCome back tomorrow!'),
|
||||||
|
_QuestList(quests: story, emptyMessage: 'Your journey has just begun.'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuestList extends StatelessWidget {
|
||||||
|
final List<QuestCollection> quests;
|
||||||
|
final String emptyMessage;
|
||||||
|
|
||||||
|
const _QuestList({required this.quests, required this.emptyMessage});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (quests.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
emptyMessage,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: quests.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return QuestItem(quest: quests[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
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 '../../../gamification/data/repositories/quest_repository.dart';
|
||||||
|
import '../../../../shared/data/local/app_database.dart';
|
||||||
|
|
||||||
|
class QuestBoardWidget extends ConsumerWidget {
|
||||||
|
const QuestBoardWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final questRepo = ref.watch(questRepositoryProvider);
|
||||||
|
|
||||||
|
return StreamBuilder<List<QuestCollection>>(
|
||||||
|
stream: questRepo.watchQuests(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final allQuests = snapshot.data ?? [];
|
||||||
|
// Nur aktive Dailies anzeigen, max 3
|
||||||
|
final dailies = allQuests
|
||||||
|
.where((q) => q.type == 'daily' && !q.isClaimed)
|
||||||
|
.take(3)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (dailies.isEmpty) return const SizedBox.shrink(); // Ausblenden wenn leer
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surfaceColor, // Oder ein "Holz" Texture für RPG Look
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white10),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'DAILY BOUNTIES',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: AppTheme.secondaryColor,
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => context.go('/quests'),
|
||||||
|
child: const Text('VIEW ALL >', style: TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...dailies.map((q) => _MiniQuestRow(quest: q)).toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MiniQuestRow extends StatelessWidget {
|
||||||
|
final QuestCollection quest;
|
||||||
|
const _MiniQuestRow({required this.quest});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final progress = (quest.currentValue / quest.targetValue).clamp(0.0, 1.0);
|
||||||
|
final isComplete = quest.isCompleted;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isComplete ? Icons.check_circle : Icons.circle_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: isComplete ? AppTheme.successColor : Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
quest.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isComplete ? Colors.grey : Colors.white,
|
||||||
|
decoration: isComplete ? TextDecoration.lineThrough : null,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isComplete)
|
||||||
|
SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Colors.white10,
|
||||||
|
color: AppTheme.secondaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../../shared/data/local/app_database.dart'; // Für QuestCollection Klasse
|
||||||
|
import '../../data/repositories/quest_repository.dart';
|
||||||
|
import '../../domain/entities/item_catalog.dart';
|
||||||
|
|
||||||
|
class QuestItem extends ConsumerStatefulWidget {
|
||||||
|
final QuestCollection quest;
|
||||||
|
|
||||||
|
const QuestItem({
|
||||||
|
super.key,
|
||||||
|
required this.quest,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<QuestItem> createState() => _QuestItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuestItemState extends ConsumerState<QuestItem> {
|
||||||
|
bool _isClaiming = false;
|
||||||
|
|
||||||
|
Future<void> _handleClaim() async {
|
||||||
|
setState(() => _isClaiming = true);
|
||||||
|
try {
|
||||||
|
// 1. XP und Item gutschreiben (Logik im Repo oder Service wäre besser,
|
||||||
|
// aber für MVP machen wir den Claim im Repo und User-Update hier oder im Service).
|
||||||
|
// Einfachheitshalber: Repo setzt isClaimed=true. Wir müssen aber auch XP geben.
|
||||||
|
// BESSER: Wir nutzen einen QuestService Methode 'claimReward', die beides macht.
|
||||||
|
// Da wir die noch nicht haben, machen wir es hier "manuell" via Repos.
|
||||||
|
|
||||||
|
final questRepo = ref.read(questRepositoryProvider);
|
||||||
|
await questRepo.claimQuest(widget.quest.id);
|
||||||
|
|
||||||
|
// Wir verlassen uns darauf, dass der UserRepo/XP Service das separat regelt
|
||||||
|
// oder wir feuern hier ein Event.
|
||||||
|
// Für das UI Feedback reicht erst mal das Claimen.
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check_circle, color: AppTheme.successColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('Reward collected: ${widget.quest.rewardXP} XP!'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black87,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isClaiming = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final progress = (widget.quest.currentValue / widget.quest.targetValue).clamp(0.0, 1.0);
|
||||||
|
final isComplete = widget.quest.isCompleted;
|
||||||
|
final isClaimed = widget.quest.isClaimed;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: isComplete && !isClaimed
|
||||||
|
? const BorderSide(color: AppTheme.successColor, width: 1)
|
||||||
|
: BorderSide.none,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_getIconForType(widget.quest.type),
|
||||||
|
color: isComplete ? AppTheme.successColor : AppTheme.primaryColor,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.quest.title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isClaimed ? Colors.grey : Colors.white,
|
||||||
|
decoration: isClaimed ? TextDecoration.lineThrough : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isClaimed)
|
||||||
|
const Icon(Icons.check, color: Colors.grey, size: 20)
|
||||||
|
else if (widget.quest.rewardXP > 0)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.xpBarFill.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'+${widget.quest.rewardXP} XP',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
widget.quest.description,
|
||||||
|
style: TextStyle(color: isClaimed ? Colors.grey : AppTheme.textSecondary, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Progress Bar & Action
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Colors.grey[800],
|
||||||
|
color: isComplete ? AppTheme.successColor : AppTheme.primaryColor,
|
||||||
|
minHeight: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${widget.quest.currentValue} / ${widget.quest.targetValue}',
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
|
if (isComplete && !isClaimed)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isClaiming ? null : _handleClaim,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.successColor,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||||
|
minimumSize: const Size(0, 32),
|
||||||
|
),
|
||||||
|
child: _isClaiming
|
||||||
|
? const SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||||
|
: const Text('CLAIM', style: TextStyle(fontSize: 12)),
|
||||||
|
)
|
||||||
|
else if (widget.quest.rewardItem != null && !isClaimed)
|
||||||
|
const Icon(Icons.inventory_2, color: AppTheme.secondaryColor, size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getIconForType(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'daily': return Icons.today;
|
||||||
|
case 'story': return Icons.auto_stories;
|
||||||
|
case 'milestone': return Icons.emoji_events;
|
||||||
|
default: return Icons.task_alt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ import '../../../../shared/data/remote/sync_service.dart';
|
||||||
import '../widgets/plate_visualizer.dart';
|
import '../widgets/plate_visualizer.dart';
|
||||||
import '../widgets/timer_widget.dart';
|
import '../widgets/timer_widget.dart';
|
||||||
import '../widgets/enemy_hp_bar.dart';
|
import '../widgets/enemy_hp_bar.dart';
|
||||||
|
import '../../../gamification/application/quest_service.dart';
|
||||||
|
|
||||||
class BattleScreen extends ConsumerStatefulWidget {
|
class BattleScreen extends ConsumerStatefulWidget {
|
||||||
final int week;
|
final int week;
|
||||||
|
|
@ -274,6 +275,26 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final questService = ref.read(questServiceProvider);
|
||||||
|
|
||||||
|
await questService.reportEvent(QuestTrigger.workoutComplete);
|
||||||
|
|
||||||
|
int totalVolume = 0;
|
||||||
|
int totalReps = 0;
|
||||||
|
for (var ex in _exercises) {
|
||||||
|
for (var set in ex.sets) {
|
||||||
|
if (set.completed) {
|
||||||
|
totalVolume += (set.targetWeightTotal * set.repsActual).round();
|
||||||
|
totalReps += set.repsActual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalVolume > 0)
|
||||||
|
await questService.reportEvent(QuestTrigger.volume, data: totalVolume);
|
||||||
|
if (totalReps > 0)
|
||||||
|
await questService.reportEvent(QuestTrigger.repCount, data: totalReps);
|
||||||
|
|
||||||
if (widget.workoutId != null) {
|
if (widget.workoutId != null) {
|
||||||
final workoutRepo = ref.read(workoutRepositoryProvider);
|
final workoutRepo = ref.read(workoutRepositoryProvider);
|
||||||
final cycleRepo = ref.read(cycleRepositoryProvider);
|
final cycleRepo = ref.read(cycleRepositoryProvider);
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,23 @@ import 'converters/json_converter.dart';
|
||||||
|
|
||||||
part 'app_database.g.dart';
|
part 'app_database.g.dart';
|
||||||
|
|
||||||
@DriftDatabase(tables: [Users, Cycles, Workouts])
|
@DriftDatabase(tables: [Users, Cycles, Workouts, Quests])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase() : super(_openConnection());
|
AppDatabase() : super(_openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 2;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
onCreate: (Migrator m) async {
|
onCreate: (Migrator m) async {
|
||||||
await m.createAll();
|
await m.createAll();
|
||||||
},
|
},
|
||||||
|
onUpgrade: (Migrator m, int from, int to) async {
|
||||||
|
if (from < 2) {
|
||||||
|
await m.createTable(quests);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -64,3 +64,27 @@ class Workouts extends Table {
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DataClassName('QuestCollection')
|
||||||
|
class Quests extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
TextColumn get type => text()(); // 'daily', 'milestone', 'story'
|
||||||
|
TextColumn get title => text()();
|
||||||
|
TextColumn get description => text()();
|
||||||
|
|
||||||
|
IntColumn get targetValue => integer()();
|
||||||
|
IntColumn get currentValue => integer().withDefault(const Constant(0))();
|
||||||
|
|
||||||
|
IntColumn get rewardXP => integer()();
|
||||||
|
TextColumn get rewardItem => text().nullable()();
|
||||||
|
|
||||||
|
BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get isClaimed => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
|
DateTimeColumn get expiresAt => dateTime().nullable()();
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,233 @@
|
||||||
|
// import 'dart:convert';
|
||||||
|
// import 'package:flutter/foundation.dart';
|
||||||
|
// import 'package:flutter_riverpod/flutter_riverpod.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/app_database.dart';
|
||||||
|
// import '../repositories/user_repository.dart';
|
||||||
|
// import 'api_client.dart';
|
||||||
|
|
||||||
|
// final syncServiceProvider = Provider<SyncService>((ref) {
|
||||||
|
// final db = ref.watch(appDatabaseProvider);
|
||||||
|
// final apiClient = ref.watch(apiClientProvider);
|
||||||
|
// return SyncService(db: db, apiClient: apiClient);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// class SyncService {
|
||||||
|
// final AppDatabase db;
|
||||||
|
// final ApiClient apiClient;
|
||||||
|
// final _storage = const FlutterSecureStorage();
|
||||||
|
// bool _isSyncing = false;
|
||||||
|
|
||||||
|
// SyncService({required this.db, required this.apiClient});
|
||||||
|
|
||||||
|
// Future<void> sync() async {
|
||||||
|
// if (_isSyncing) return;
|
||||||
|
// _isSyncing = true;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// debugPrint('🔄 Starting Sync...');
|
||||||
|
|
||||||
|
// 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...');
|
||||||
|
// final tmsMap = cycle.trainingMaxes
|
||||||
|
// .map((k, v) => MapEntry(k, (v as num).toDouble()));
|
||||||
|
|
||||||
|
// final response = await apiClient.createCycle(tmsMap);
|
||||||
|
// final newServerId = response['id'];
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
// 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 (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||||
|
// .write(
|
||||||
|
// const CyclesCompanion(isDirty: Value(false)),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// debugPrint('❌ Failed to sync cycle ${cycle.id}: $e');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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 validWorkouts =
|
||||||
|
// dirtyWorkouts.where((w) => w.cycleId.length > 5).toList();
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 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)),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// 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']['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, stack) {
|
||||||
|
// debugPrint('❌ Sync failed: $e');
|
||||||
|
// debugPrint(stack.toString());
|
||||||
|
// } finally {
|
||||||
|
// _isSyncing = false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
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';
|
||||||
|
|
@ -31,6 +261,7 @@ class SyncService {
|
||||||
try {
|
try {
|
||||||
debugPrint('🔄 Starting Sync...');
|
debugPrint('🔄 Starting Sync...');
|
||||||
|
|
||||||
|
// 1. CYCLES SYNC
|
||||||
final dirtyCycles = await (db.select(db.cycles)
|
final dirtyCycles = await (db.select(db.cycles)
|
||||||
..where((c) => c.isDirty.equals(true)))
|
..where((c) => c.isDirty.equals(true)))
|
||||||
.get();
|
.get();
|
||||||
|
|
@ -38,8 +269,7 @@ class SyncService {
|
||||||
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}...');
|
||||||
'📤 Pushing new cycle ${cycle.cycleNumber} to server...');
|
|
||||||
final tmsMap = cycle.trainingMaxes
|
final tmsMap = cycle.trainingMaxes
|
||||||
.map((k, v) => MapEntry(k, (v as num).toDouble()));
|
.map((k, v) => MapEntry(k, (v as num).toDouble()));
|
||||||
|
|
||||||
|
|
@ -55,6 +285,7 @@ class SyncService {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Relink workouts
|
||||||
final oldLocalIdRef = cycle.id.toString();
|
final oldLocalIdRef = cycle.id.toString();
|
||||||
await (db.update(db.workouts)
|
await (db.update(db.workouts)
|
||||||
..where((w) => w.cycleId.equals(oldLocalIdRef)))
|
..where((w) => w.cycleId.equals(oldLocalIdRef)))
|
||||||
|
|
@ -64,20 +295,17 @@ class SyncService {
|
||||||
isDirty: const Value(true),
|
isDirty: const Value(true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
debugPrint(
|
|
||||||
'🔗 Relinked workouts from local cycle $oldLocalIdRef to server $newServerId');
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
|
||||||
.write(
|
.write(const CyclesCompanion(isDirty: Value(false)));
|
||||||
const CyclesCompanion(isDirty: Value(false)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Failed to sync cycle ${cycle.id}: $e');
|
debugPrint('❌ Failed to sync cycle ${cycle.id}: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. USER & WORKOUTS SYNC
|
||||||
final dirtyUser = await (db.select(db.users)
|
final dirtyUser = await (db.select(db.users)
|
||||||
..where((u) => u.isDirty.equals(true)))
|
..where((u) => u.isDirty.equals(true)))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
|
|
@ -113,112 +341,130 @@ class SyncService {
|
||||||
|
|
||||||
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
|
||||||
|
|
||||||
debugPrint(
|
if ((pushData['workouts'] as List).isNotEmpty ||
|
||||||
'☁️ Contacting server (Push: ${validWorkouts.length} workouts)...');
|
pushData['user_stats'] != null) {
|
||||||
|
debugPrint('📤 Pushing data...');
|
||||||
final response = await apiClient.sync(
|
final response = await apiClient.sync(
|
||||||
lastSyncTimestamp: lastSync ?? '',
|
lastSyncTimestamp: lastSync ?? '',
|
||||||
pushData: pushData,
|
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)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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']['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'],
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
await db.transaction(() async {
|
||||||
|
// Clean dirty flags
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROCESS PULL DATA
|
||||||
|
if (response['pull_data'] != null) {
|
||||||
|
// Cycles Pull
|
||||||
|
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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workouts Pull - MIT DUPLIKAT-SCHUTZ
|
||||||
|
if (response['pull_data']['workouts'] != null) {
|
||||||
|
final pulledWorkouts = response['pull_data']['workouts'] as List;
|
||||||
|
debugPrint('📥 Pulled ${pulledWorkouts.length} workouts.');
|
||||||
|
|
||||||
|
for (var wJson in pulledWorkouts) {
|
||||||
|
final serverId = wJson['id'] as String;
|
||||||
|
final cycleId = wJson['cycle_id'] as String;
|
||||||
|
final week = wJson['week'] as int;
|
||||||
|
final day = wJson['day'] as int;
|
||||||
|
|
||||||
|
// 1. Versuch: Match über Server ID
|
||||||
|
var existing = await (db.select(db.workouts)
|
||||||
|
..where((w) => w.serverId.equals(serverId)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
// 2. Versuch: Match über Logik (Cycle + Week + Day)
|
||||||
|
// Das verhindert Duplikate, wenn ServerID lokal noch fehlt
|
||||||
|
if (existing == null) {
|
||||||
|
final candidates = await (db.select(db.workouts)
|
||||||
|
..where((w) =>
|
||||||
|
w.cycleId.equals(cycleId) &
|
||||||
|
w.week.equals(week) &
|
||||||
|
w.day.equals(day)))
|
||||||
|
.get();
|
||||||
|
if (candidates.isNotEmpty) {
|
||||||
|
existing = candidates.first;
|
||||||
|
debugPrint(
|
||||||
|
'🔄 Merging local workout ${existing.id} with server ID $serverId');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final companion = WorkoutsCompanion(
|
||||||
|
serverId: Value(serverId),
|
||||||
|
cycleId: Value(cycleId),
|
||||||
|
userId: Value(wJson['user_id']),
|
||||||
|
week: Value(week),
|
||||||
|
day: Value(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');
|
debugPrint('✅ Sync completed successfully');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
debugPrint('❌ Sync failed: $e');
|
debugPrint('❌ Sync failed: $e');
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,109 @@
|
||||||
|
// import 'package:flutter_riverpod/flutter_riverpod.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 db = ref.watch(appDatabaseProvider);
|
||||||
|
// final apiClient = ref.watch(apiClientProvider);
|
||||||
|
// return WorkoutRepository(db: db, apiClient: apiClient);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// class WorkoutRepository {
|
||||||
|
// final AppDatabase db;
|
||||||
|
// final ApiClient apiClient;
|
||||||
|
|
||||||
|
// WorkoutRepository({required this.db, required this.apiClient});
|
||||||
|
|
||||||
|
// Future<List<WorkoutCollection>> getAllWorkouts() async {
|
||||||
|
// return await db.select(db.workouts).get();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Future<List<WorkoutCollection>> getWorkoutsForCycle(String cycleId) async {
|
||||||
|
// return await (db.select(db.workouts)
|
||||||
|
// ..where((w) => w.cycleId.equals(cycleId)))
|
||||||
|
// .get();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Future<List<WorkoutCollection>> getCompletedWorkouts(String userId) async {
|
||||||
|
// return await (db.select(db.workouts)
|
||||||
|
// ..where((w) => w.userId.equals(userId) & w.completedAt.isNotNull()))
|
||||||
|
// .get();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Future<void> saveWorkout(WorkoutCollection workout) async {
|
||||||
|
// final companion = workout.toCompanion(true).copyWith(
|
||||||
|
// updatedAt: Value(DateTime.now()),
|
||||||
|
// isDirty: const Value(true),
|
||||||
|
// );
|
||||||
|
// await db.into(db.workouts).insertOnConflictUpdate(companion);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Future<WorkoutCollection> createWorkout({
|
||||||
|
// required String userId,
|
||||||
|
// required String cycleId,
|
||||||
|
// required int week,
|
||||||
|
// required int day,
|
||||||
|
// required List<dynamic> exercises,
|
||||||
|
// }) async {
|
||||||
|
// 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()),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// 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({
|
||||||
|
// required String cycleId,
|
||||||
|
// String? localCycleId,
|
||||||
|
// required int week,
|
||||||
|
// required int day,
|
||||||
|
// }) async {
|
||||||
|
// 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();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import '../local/app_database.dart';
|
import '../local/app_database.dart';
|
||||||
|
|
@ -100,7 +206,8 @@ class WorkoutRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
return weekDayCheck & cycleCheck;
|
return weekDayCheck & cycleCheck;
|
||||||
}))
|
})
|
||||||
|
..limit(1))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,12 @@ mixin _$Exercise {
|
||||||
double get bodyweightAtSession => throw _privateConstructorUsedError;
|
double get bodyweightAtSession => throw _privateConstructorUsedError;
|
||||||
List<WorkoutSet> get sets => throw _privateConstructorUsedError;
|
List<WorkoutSet> get sets => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this Exercise to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
|
||||||
|
/// Create a copy of Exercise
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
$ExerciseCopyWith<Exercise> get copyWith =>
|
$ExerciseCopyWith<Exercise> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +57,8 @@ class _$ExerciseCopyWithImpl<$Res, $Val extends Exercise>
|
||||||
// ignore: unused_field
|
// ignore: unused_field
|
||||||
final $Res Function($Val) _then;
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of Exercise
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
|
|
@ -105,6 +111,8 @@ class __$$ExerciseImplCopyWithImpl<$Res>
|
||||||
_$ExerciseImpl _value, $Res Function(_$ExerciseImpl) _then)
|
_$ExerciseImpl _value, $Res Function(_$ExerciseImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of Exercise
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
|
|
@ -182,12 +190,14 @@ class _$ExerciseImpl implements _Exercise {
|
||||||
const DeepCollectionEquality().equals(other._sets, _sets));
|
const DeepCollectionEquality().equals(other._sets, _sets));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, exerciseId, exerciseName,
|
int get hashCode => Object.hash(runtimeType, exerciseId, exerciseName,
|
||||||
bodyweightAtSession, const DeepCollectionEquality().hash(_sets));
|
bodyweightAtSession, const DeepCollectionEquality().hash(_sets));
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
/// Create a copy of Exercise
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith =>
|
_$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith =>
|
||||||
|
|
@ -219,8 +229,11 @@ abstract class _Exercise implements Exercise {
|
||||||
double get bodyweightAtSession;
|
double get bodyweightAtSession;
|
||||||
@override
|
@override
|
||||||
List<WorkoutSet> get sets;
|
List<WorkoutSet> get sets;
|
||||||
|
|
||||||
|
/// Create a copy of Exercise
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
_$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith =>
|
_$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,12 @@ mixin _$TrainingMaxes {
|
||||||
double get pullup => throw _privateConstructorUsedError;
|
double get pullup => throw _privateConstructorUsedError;
|
||||||
double get dip => throw _privateConstructorUsedError;
|
double get dip => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this TrainingMaxes to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
|
||||||
|
/// Create a copy of TrainingMaxes
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
$TrainingMaxesCopyWith<TrainingMaxes> get copyWith =>
|
$TrainingMaxesCopyWith<TrainingMaxes> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +53,8 @@ class _$TrainingMaxesCopyWithImpl<$Res, $Val extends TrainingMaxes>
|
||||||
// ignore: unused_field
|
// ignore: unused_field
|
||||||
final $Res Function($Val) _then;
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of TrainingMaxes
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
|
|
@ -92,6 +98,8 @@ class __$$TrainingMaxesImplCopyWithImpl<$Res>
|
||||||
_$TrainingMaxesImpl _value, $Res Function(_$TrainingMaxesImpl) _then)
|
_$TrainingMaxesImpl _value, $Res Function(_$TrainingMaxesImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of TrainingMaxes
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
|
|
@ -150,11 +158,13 @@ class _$TrainingMaxesImpl implements _TrainingMaxes {
|
||||||
(identical(other.dip, dip) || other.dip == dip));
|
(identical(other.dip, dip) || other.dip == dip));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, squat, pullup, dip);
|
int get hashCode => Object.hash(runtimeType, squat, pullup, dip);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
/// Create a copy of TrainingMaxes
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$TrainingMaxesImplCopyWith<_$TrainingMaxesImpl> get copyWith =>
|
_$$TrainingMaxesImplCopyWith<_$TrainingMaxesImpl> get copyWith =>
|
||||||
|
|
@ -183,8 +193,11 @@ abstract class _TrainingMaxes implements TrainingMaxes {
|
||||||
double get pullup;
|
double get pullup;
|
||||||
@override
|
@override
|
||||||
double get dip;
|
double get dip;
|
||||||
|
|
||||||
|
/// Create a copy of TrainingMaxes
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
_$$TrainingMaxesImplCopyWith<_$TrainingMaxesImpl> get copyWith =>
|
_$$TrainingMaxesImplCopyWith<_$TrainingMaxesImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,12 @@ mixin _$WorkoutSet {
|
||||||
bool get completed => throw _privateConstructorUsedError;
|
bool get completed => throw _privateConstructorUsedError;
|
||||||
int? get rpe => throw _privateConstructorUsedError;
|
int? get rpe => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this WorkoutSet to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
|
||||||
|
/// Create a copy of WorkoutSet
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
$WorkoutSetCopyWith<WorkoutSet> get copyWith =>
|
$WorkoutSetCopyWith<WorkoutSet> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +68,8 @@ class _$WorkoutSetCopyWithImpl<$Res, $Val extends WorkoutSet>
|
||||||
// ignore: unused_field
|
// ignore: unused_field
|
||||||
final $Res Function($Val) _then;
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of WorkoutSet
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
|
|
@ -146,6 +152,8 @@ class __$$WorkoutSetImplCopyWithImpl<$Res>
|
||||||
_$WorkoutSetImpl _value, $Res Function(_$WorkoutSetImpl) _then)
|
_$WorkoutSetImpl _value, $Res Function(_$WorkoutSetImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of WorkoutSet
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
|
|
@ -272,7 +280,7 @@ class _$WorkoutSetImpl implements _WorkoutSet {
|
||||||
(identical(other.rpe, rpe) || other.rpe == rpe));
|
(identical(other.rpe, rpe) || other.rpe == rpe));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
runtimeType,
|
runtimeType,
|
||||||
|
|
@ -286,7 +294,9 @@ class _$WorkoutSetImpl implements _WorkoutSet {
|
||||||
completed,
|
completed,
|
||||||
rpe);
|
rpe);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
/// Create a copy of WorkoutSet
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$WorkoutSetImplCopyWith<_$WorkoutSetImpl> get copyWith =>
|
_$$WorkoutSetImplCopyWith<_$WorkoutSetImpl> get copyWith =>
|
||||||
|
|
@ -333,8 +343,11 @@ abstract class _WorkoutSet implements WorkoutSet {
|
||||||
bool get completed;
|
bool get completed;
|
||||||
@override
|
@override
|
||||||
int? get rpe;
|
int? get rpe;
|
||||||
|
|
||||||
|
/// Create a copy of WorkoutSet
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
_$$WorkoutSetImplCopyWith<_$WorkoutSetImpl> get copyWith =>
|
_$$WorkoutSetImplCopyWith<_$WorkoutSetImpl> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
84
pubspec.lock
84
pubspec.lock
|
|
@ -5,26 +5,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "67.0.0"
|
version: "85.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.4.1"
|
version: "7.6.0"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer_plugin
|
name: analyzer_plugin
|
||||||
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
|
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.3"
|
version: "0.13.4"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -53,10 +53,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
|
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.5.4"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -77,26 +77,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_resolvers
|
name: build_resolvers
|
||||||
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
|
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.5.4"
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
|
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.13"
|
version: "2.5.4"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_runner_core
|
name: build_runner_core
|
||||||
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
|
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.2"
|
version: "9.1.2"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -221,18 +221,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_core
|
name: custom_lint_core
|
||||||
sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
|
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.3"
|
version: "0.7.5"
|
||||||
|
custom_lint_visitor:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: custom_lint_visitor
|
||||||
|
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0+7.7.0"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
|
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "3.1.1"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -253,18 +261,18 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: drift
|
name: drift
|
||||||
sha256: df027d168a2985a2e9da900adeba2ab0136f0d84436592cf3cd5135f82c8579c
|
sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.21.0"
|
version: "2.28.2"
|
||||||
drift_dev:
|
drift_dev:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
sha256: "623649abe932fc17bd32e578e7e05f7ac5e7dd0b33e6c8669a0634105d1389bf"
|
sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.21.2"
|
version: "2.28.0"
|
||||||
drift_flutter:
|
drift_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -420,10 +428,10 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: freezed
|
name: freezed
|
||||||
sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1
|
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.2"
|
version: "2.5.8"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -532,10 +540,10 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: json_serializable
|
name: json_serializable
|
||||||
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
|
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.8.0"
|
version: "6.9.5"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -772,10 +780,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod_analyzer_utils
|
name: riverpod_analyzer_utils
|
||||||
sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f"
|
sha256: "837a6dc33f490706c7f4632c516bcd10804ee4d9ccc8046124ca56388715fdf3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1"
|
version: "0.5.9"
|
||||||
riverpod_annotation:
|
riverpod_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -788,10 +796,10 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: riverpod_generator
|
name: riverpod_generator
|
||||||
sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22
|
sha256: "120d3310f687f43e7011bb213b90a436f1bbc300f0e4b251a72c39bccb017a4f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.6.4"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -868,10 +876,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shelf_web_socket
|
name: shelf_web_socket
|
||||||
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
|
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "3.0.0"
|
||||||
shimmer:
|
shimmer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -889,18 +897,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "2.0.0"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_helper
|
name: source_helper
|
||||||
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.5"
|
version: "1.3.7"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -969,10 +977,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlparser
|
name: sqlparser
|
||||||
sha256: d77749237609784e337ec36c979d41f6f38a7b279df98622ae23929c8eb954a4
|
sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.39.2"
|
version: "0.41.2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue