feat: add basic quest engine

This commit is contained in:
Patryk Hegenberg 2025-12-01 14:26:10 +01:00
parent ee89f327bd
commit 311d764a4d
23 changed files with 5056 additions and 1449 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
import '../../features/authentication/presentation/screens/login_screen.dart';
import '../../features/authentication/presentation/screens/profile_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/welcome_screen.dart';
import '../../features/onboarding/presentation/screens/bodyweight_input_screen.dart';
@ -113,6 +114,11 @@ final routerProvider = Provider<GoRouter>((ref) {
name: 'codex',
builder: (context, state) => const CodexScreen(),
),
GoRoute(
path: '/quests',
name: 'quests',
builder: (context, state) => const QuestLogScreen(),
),
],
);
});

View file

@ -9,6 +9,7 @@ import '../../../../shared/data/local/app_database.dart';
import '../../../gamification/domain/entities/avatar_config.dart';
import '../../../gamification/presentation/widgets/avatar_editor.dart';
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
import '../../../gamification/domain/entities/item_catalog.dart';
class ProfileScreen extends ConsumerStatefulWidget {
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(
String title, String content, VoidCallback onConfirm) {
showDialog(
@ -270,6 +411,15 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
),
),
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',
style: Theme.of(context)
.textTheme

View file

@ -16,10 +16,13 @@ import '../../../../shared/domain/logic/wendler_calculator.dart';
import '../../../../shared/domain/entities/exercise.dart';
import '../../../../shared/domain/entities/workout_set.dart';
import '../../../gamification/domain/entities/avatar_config.dart';
import '../../../gamification/domain/entities/item_catalog.dart';
import '../../../gamification/presentation/widgets/avatar_renderer.dart';
import '../../../gamification/presentation/widgets/quest_board.dart';
import '../widgets/xp_bar_widget.dart';
import '../widgets/level_display.dart';
import '../widgets/start_raid_button.dart';
import '../../../gamification/application/quest_service.dart';
class HubScreen extends ConsumerStatefulWidget {
const HubScreen({super.key});
@ -40,8 +43,9 @@ class _HubScreenState extends ConsumerState<HubScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_runSync();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _runSync();
await ref.read(questServiceProvider).checkAndGenerateQuests();
});
}
@ -205,6 +209,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
final avatarConfig = user?.avatarConfig != null
? AvatarConfig.fromJson(user!.avatarConfig!)
: const AvatarConfig();
final bgItem =
ItemCatalog.getBackground(avatarConfig.selectedBackground);
if (user == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -223,10 +229,18 @@ class _HubScreenState extends ConsumerState<HubScreen> {
children: [
Positioned.fill(
child: Image.asset(
AssetPaths.bgStreetParkDay,
bgItem.assetPath,
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(
child: Container(
decoration: BoxDecoration(
@ -289,6 +303,8 @@ class _HubScreenState extends ConsumerState<HubScreen> {
nextLevelXP: nextLevelXP,
),
),
const SizedBox(height: 16),
const QuestBoardWidget(),
const Spacer(flex: 2),
if (cycle != null)
Padding(

View 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),
];

View file

@ -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();
}
}

View file

@ -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';
class AvatarConfig {
final String gender; // 'male' or 'female'
final int variant; // 1 to 8
final String gender;
final int variant;
final String selectedBackground; // NEU
const AvatarConfig({
this.gender = 'male',
this.variant = 1,
this.selectedBackground = 'bg_street_day', // Default
});
factory AvatarConfig.fromJson(Map<String, dynamic> json) {
return AvatarConfig(
gender: json['gender'] ?? 'male',
variant: json['variant'] ?? 1,
selectedBackground: json['selected_background'] ?? 'bg_street_day', // NEU
);
}
@ -20,6 +52,7 @@ class AvatarConfig {
return {
'gender': gender,
'variant': variant,
'selected_background': selectedBackground, // NEU
};
}

View file

@ -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
);
}
}

View file

@ -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]);
},
);
}
}

View file

@ -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,
),
),
],
),
);
}
}

View file

@ -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;
}
}
}

View file

@ -17,6 +17,7 @@ import '../../../../shared/data/remote/sync_service.dart';
import '../widgets/plate_visualizer.dart';
import '../widgets/timer_widget.dart';
import '../widgets/enemy_hp_bar.dart';
import '../../../gamification/application/quest_service.dart';
class BattleScreen extends ConsumerStatefulWidget {
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) {
final workoutRepo = ref.read(workoutRepositoryProvider);
final cycleRepo = ref.read(cycleRepositoryProvider);

View file

@ -8,18 +8,23 @@ import 'converters/json_converter.dart';
part 'app_database.g.dart';
@DriftDatabase(tables: [Users, Cycles, Workouts])
@DriftDatabase(tables: [Users, Cycles, Workouts, Quests])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
int get schemaVersion => 2;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) async {
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

View file

@ -64,3 +64,27 @@ class Workouts extends Table {
DateTimeColumn get createdAt => 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};
}

View file

@ -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 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -31,6 +261,7 @@ class SyncService {
try {
debugPrint('🔄 Starting Sync...');
// 1. CYCLES SYNC
final dirtyCycles = await (db.select(db.cycles)
..where((c) => c.isDirty.equals(true)))
.get();
@ -38,8 +269,7 @@ class SyncService {
for (var cycle in dirtyCycles) {
try {
if (cycle.serverId == null) {
debugPrint(
'📤 Pushing new cycle ${cycle.cycleNumber} to server...');
debugPrint('📤 Pushing new cycle ${cycle.cycleNumber}...');
final tmsMap = cycle.trainingMaxes
.map((k, v) => MapEntry(k, (v as num).toDouble()));
@ -55,6 +285,7 @@ class SyncService {
),
);
// Relink workouts
final oldLocalIdRef = cycle.id.toString();
await (db.update(db.workouts)
..where((w) => w.cycleId.equals(oldLocalIdRef)))
@ -64,20 +295,17 @@ class SyncService {
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)),
);
.write(const CyclesCompanion(isDirty: Value(false)));
}
} catch (e) {
debugPrint('❌ Failed to sync cycle ${cycle.id}: $e');
}
}
// 2. USER & WORKOUTS SYNC
final dirtyUser = await (db.select(db.users)
..where((u) => u.isDirty.equals(true)))
.getSingleOrNull();
@ -113,29 +341,28 @@ class SyncService {
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
debugPrint(
'☁️ Contacting server (Push: ${validWorkouts.length} workouts)...');
if ((pushData['workouts'] as List).isNotEmpty ||
pushData['user_stats'] != null) {
debugPrint('📤 Pushing data...');
final response = await apiClient.sync(
lastSyncTimestamp: lastSync ?? '',
pushData: pushData,
);
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)),
);
.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)),
);
.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) {
@ -145,7 +372,6 @@ class SyncService {
.getSingleOrNull();
final tms = cJson['training_maxes'] as Map<String, dynamic>;
final companion = CyclesCompanion(
serverId: Value(serverId),
userId: Value(cJson['user_id']),
@ -171,23 +397,44 @@ class SyncService {
}
}
// Workouts Pull - MIT DUPLIKAT-SCHUTZ
if (response['pull_data']['workouts'] != null) {
final pulledWorkouts = response['pull_data']['workouts'] as List;
debugPrint(
'📥 Pulled ${pulledWorkouts.length} workouts from server.');
debugPrint('📥 Pulled ${pulledWorkouts.length} workouts.');
for (var wJson in pulledWorkouts) {
final serverId = wJson['id'] as String;
final existing = await (db.select(db.workouts)
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(wJson['cycle_id']),
cycleId: Value(cycleId),
userId: Value(wJson['user_id']),
week: Value(wJson['week']),
day: Value(wJson['day']),
week: Value(week),
day: Value(day),
completedAt:
Value(DateTime.tryParse(wJson['completed_at'] ?? '')),
xpEarned: Value(wJson['xp_earned'] ?? 0),
@ -202,7 +449,7 @@ class SyncService {
if (existing != null) {
await (db.update(db.workouts)
..where((w) => w.id.equals(existing.id)))
..where((w) => w.id.equals(existing!.id)))
.write(companion);
} else {
await db.into(db.workouts).insert(companion);
@ -215,10 +462,9 @@ class SyncService {
if (response['server_timestamp'] != null) {
await _storage.write(
key: AppConstants.keyLastSync,
value: response['server_timestamp'],
);
value: response['server_timestamp']);
}
}
debugPrint('✅ Sync completed successfully');
} catch (e, stack) {
debugPrint('❌ Sync failed: $e');

View file

@ -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:drift/drift.dart';
import '../local/app_database.dart';
@ -100,7 +206,8 @@ class WorkoutRepository {
}
return weekDayCheck & cycleCheck;
}))
})
..limit(1))
.getSingleOrNull();
}
}

View file

@ -25,8 +25,12 @@ mixin _$Exercise {
double get bodyweightAtSession => throw _privateConstructorUsedError;
List<WorkoutSet> get sets => throw _privateConstructorUsedError;
/// Serializes this Exercise to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -53,6 +57,8 @@ class _$ExerciseCopyWithImpl<$Res, $Val extends Exercise>
// ignore: unused_field
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')
@override
$Res call({
@ -105,6 +111,8 @@ class __$$ExerciseImplCopyWithImpl<$Res>
_$ExerciseImpl _value, $Res Function(_$ExerciseImpl) _then)
: super(_value, _then);
/// Create a copy of Exercise
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -182,12 +190,14 @@ class _$ExerciseImpl implements _Exercise {
const DeepCollectionEquality().equals(other._sets, _sets));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, exerciseId, exerciseName,
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
@pragma('vm:prefer-inline')
_$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith =>
@ -219,8 +229,11 @@ abstract class _Exercise implements Exercise {
double get bodyweightAtSession;
@override
List<WorkoutSet> get sets;
/// Create a copy of Exercise
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ExerciseImplCopyWith<_$ExerciseImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -24,8 +24,12 @@ mixin _$TrainingMaxes {
double get pullup => throw _privateConstructorUsedError;
double get dip => throw _privateConstructorUsedError;
/// Serializes this TrainingMaxes to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -49,6 +53,8 @@ class _$TrainingMaxesCopyWithImpl<$Res, $Val extends TrainingMaxes>
// ignore: unused_field
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')
@override
$Res call({
@ -92,6 +98,8 @@ class __$$TrainingMaxesImplCopyWithImpl<$Res>
_$TrainingMaxesImpl _value, $Res Function(_$TrainingMaxesImpl) _then)
: super(_value, _then);
/// Create a copy of TrainingMaxes
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -150,11 +158,13 @@ class _$TrainingMaxesImpl implements _TrainingMaxes {
(identical(other.dip, dip) || other.dip == dip));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
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
@pragma('vm:prefer-inline')
_$$TrainingMaxesImplCopyWith<_$TrainingMaxesImpl> get copyWith =>
@ -183,8 +193,11 @@ abstract class _TrainingMaxes implements TrainingMaxes {
double get pullup;
@override
double get dip;
/// Create a copy of TrainingMaxes
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TrainingMaxesImplCopyWith<_$TrainingMaxesImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -30,8 +30,12 @@ mixin _$WorkoutSet {
bool get completed => throw _privateConstructorUsedError;
int? get rpe => throw _privateConstructorUsedError;
/// Serializes this WorkoutSet to a JSON map.
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 =>
throw _privateConstructorUsedError;
}
@ -64,6 +68,8 @@ class _$WorkoutSetCopyWithImpl<$Res, $Val extends WorkoutSet>
// ignore: unused_field
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')
@override
$Res call({
@ -146,6 +152,8 @@ class __$$WorkoutSetImplCopyWithImpl<$Res>
_$WorkoutSetImpl _value, $Res Function(_$WorkoutSetImpl) _then)
: super(_value, _then);
/// Create a copy of WorkoutSet
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
@ -272,7 +280,7 @@ class _$WorkoutSetImpl implements _WorkoutSet {
(identical(other.rpe, rpe) || other.rpe == rpe));
}
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
@ -286,7 +294,9 @@ class _$WorkoutSetImpl implements _WorkoutSet {
completed,
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
@pragma('vm:prefer-inline')
_$$WorkoutSetImplCopyWith<_$WorkoutSetImpl> get copyWith =>
@ -333,8 +343,11 @@ abstract class _WorkoutSet implements WorkoutSet {
bool get completed;
@override
int? get rpe;
/// Create a copy of WorkoutSet
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(ignore: true)
@JsonKey(includeFromJson: false, includeToJson: false)
_$$WorkoutSetImplCopyWith<_$WorkoutSetImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -5,26 +5,26 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
source: hosted
version: "67.0.0"
version: "85.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
url: "https://pub.dev"
source: hosted
version: "6.4.1"
version: "7.6.0"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
url: "https://pub.dev"
source: hosted
version: "0.11.3"
version: "0.13.4"
args:
dependency: transitive
description:
@ -53,10 +53,10 @@ packages:
dependency: transitive
description:
name: build
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.5.4"
build_config:
dependency: transitive
description:
@ -77,26 +77,26 @@ packages:
dependency: transitive
description:
name: build_resolvers
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.5.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
url: "https://pub.dev"
source: hosted
version: "2.4.13"
version: "2.5.4"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev"
source: hosted
version: "7.3.2"
version: "9.1.2"
built_collection:
dependency: transitive
description:
@ -221,18 +221,26 @@ packages:
dependency: transitive
description:
name: custom_lint_core
sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
url: "https://pub.dev"
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:
dependency: transitive
description:
name: dart_style
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "3.1.1"
dio:
dependency: "direct main"
description:
@ -253,18 +261,18 @@ packages:
dependency: "direct main"
description:
name: drift
sha256: df027d168a2985a2e9da900adeba2ab0136f0d84436592cf3cd5135f82c8579c
sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1"
url: "https://pub.dev"
source: hosted
version: "2.21.0"
version: "2.28.2"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "623649abe932fc17bd32e578e7e05f7ac5e7dd0b33e6c8669a0634105d1389bf"
sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4"
url: "https://pub.dev"
source: hosted
version: "2.21.2"
version: "2.28.0"
drift_flutter:
dependency: "direct main"
description:
@ -420,10 +428,10 @@ packages:
dependency: "direct dev"
description:
name: freezed
sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
url: "https://pub.dev"
source: hosted
version: "2.5.2"
version: "2.5.8"
freezed_annotation:
dependency: "direct main"
description:
@ -532,10 +540,10 @@ packages:
dependency: "direct dev"
description:
name: json_serializable
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
url: "https://pub.dev"
source: hosted
version: "6.8.0"
version: "6.9.5"
leak_tracker:
dependency: transitive
description:
@ -772,10 +780,10 @@ packages:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f"
sha256: "837a6dc33f490706c7f4632c516bcd10804ee4d9ccc8046124ca56388715fdf3"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.5.9"
riverpod_annotation:
dependency: "direct main"
description:
@ -788,10 +796,10 @@ packages:
dependency: "direct dev"
description:
name: riverpod_generator
sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22
sha256: "120d3310f687f43e7011bb213b90a436f1bbc300f0e4b251a72c39bccb017a4f"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.6.4"
rxdart:
dependency: transitive
description:
@ -868,10 +876,10 @@ packages:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.0"
shimmer:
dependency: "direct main"
description:
@ -889,18 +897,18 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "2.0.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
url: "https://pub.dev"
source: hosted
version: "1.3.5"
version: "1.3.7"
source_span:
dependency: transitive
description:
@ -969,10 +977,10 @@ packages:
dependency: transitive
description:
name: sqlparser
sha256: d77749237609784e337ec36c979d41f6f38a7b279df98622ae23929c8eb954a4
sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67"
url: "https://pub.dev"
source: hosted
version: "0.39.2"
version: "0.41.2"
stack_trace:
dependency: transitive
description: