diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 24e9e1e..c7e857b 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -5,1134 +5,1134 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml index fc8e6ba..77152dd 100644 --- a/.idea/libraries/Dart_SDK.xml +++ b/.idea/libraries/Dart_SDK.xml @@ -1,29 +1,29 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index ab15c86..760fc68 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -1,28 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/routes/stats.ts b/backend/src/routes/stats.ts index 899fa07..acbd841 100644 --- a/backend/src/routes/stats.ts +++ b/backend/src/routes/stats.ts @@ -17,6 +17,30 @@ router.get('/stats', async (req: AuthRequest, res: Response, next: NextFunction) } }); +// POST /user/stats - Update gamification stats +router.post('/stats', async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const userId = req.user!.userId; + // Expecting body to have keys matching our snake_case logic from frontend? + // Wait, the frontend sends snake_case keys now: current_level, current_xp, etc. + // But the service expects UserStatsResult which has camelCase keys! + // I need to map the request body to the service interface. + + const { current_level, current_xp, total_gold, avatar_stage } = req.body; + + const updatedStats = await statsService.updateUserStats(userId, { + currentLevel: current_level, + currentXP: current_xp, + totalGold: total_gold, + avatarStage: avatar_stage + }); + + res.json({ stats: updatedStats }); + } catch (e) { + next(e); + } +}); + // GET /user/progress - Study progress router.get('/progress', async (req: AuthRequest, res: Response, next: NextFunction) => { try { diff --git a/backend/src/services/stats.service.ts b/backend/src/services/stats.service.ts index 5a774df..cfe67ae 100644 --- a/backend/src/services/stats.service.ts +++ b/backend/src/services/stats.service.ts @@ -44,6 +44,27 @@ export async function getUserStats(userId: string): Promise { }; } +export async function updateUserStats(userId: string, stats: UserStatsResult): Promise { + const res = await pool.query( + `INSERT INTO user_stats (user_id, current_level, current_xp, total_gold, avatar_stage) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (user_id) DO UPDATE SET + current_level = EXCLUDED.current_level, + current_xp = EXCLUDED.current_xp, + total_gold = EXCLUDED.total_gold, + avatar_stage = EXCLUDED.avatar_stage + RETURNING current_level, current_xp, total_gold, avatar_stage`, + [userId, stats.currentLevel, stats.currentXP, stats.totalGold, stats.avatarStage] + ); + + return { + currentLevel: res.rows[0].current_level, + currentXP: res.rows[0].current_xp, + totalGold: res.rows[0].total_gold, + avatarStage: res.rows[0].avatar_stage + }; +} + export async function getUserProgress(userId: string): Promise { // Calculate aggregate study stats // We sum duration_seconds / 60 for minutes diff --git a/lib/features/auth/application/auth_controller.dart b/lib/features/auth/application/auth_controller.dart index 0a39141..94cf376 100644 --- a/lib/features/auth/application/auth_controller.dart +++ b/lib/features/auth/application/auth_controller.dart @@ -34,16 +34,26 @@ class AuthController extends _$AuthController { // Check for existing session on app init _checkSession(); - // Initially show unauthenticated state - // Will update if session exists - return const AuthStateUnauthenticated(); + // Initially show loading state to prevent flash of login screen + return const AuthStateLoading(); } /// Check if user session exists (called on app init) Future _checkSession() async { - final user = await _repository.getCurrentUser(); - if (user != null && ref.mounted) { - state = AuthStateAuthenticated(user); + try { + final user = await _repository.getCurrentUser(); + + if (!ref.mounted) return; + + if (user != null) { + state = AuthStateAuthenticated(user); + } else { + state = const AuthStateUnauthenticated(); + } + } catch (e) { + if (ref.mounted) { + state = const AuthStateUnauthenticated(); + } } } diff --git a/lib/features/goals/presentation/goal_detail_screen.dart b/lib/features/goals/presentation/goal_detail_screen.dart index b68f04a..9428538 100644 --- a/lib/features/goals/presentation/goal_detail_screen.dart +++ b/lib/features/goals/presentation/goal_detail_screen.dart @@ -37,11 +37,16 @@ class _GoalDetailScreenState extends ConsumerState { .read(apiGoalRepositoryProvider) .updateTask(widget.goal.id, updatedTask); - // Award gold for newly completed tasks + // Award XP for newly completed tasks if (!wasCompleted && updatedTask.isCompleted) { - final currentStats = ref.read(userStatsProvider); - final newStats = GamificationService.awardTaskRewards(currentStats); - ref.read(userStatsProvider.notifier).updateStats(newStats); + // Award rewards logic + ref.read(userStatsProvider.notifier).awardTaskRewards(); + + // Get XP amount for popup display + final currentStage = ref.read(userStatsProvider).stage; + final xpEarned = GamificationService.calculateTaskXpReward( + currentStage, + ); // Show reward popup if (mounted) { @@ -49,10 +54,8 @@ class _GoalDetailScreenState extends ConsumerState { showDialog( context: context, builder: (context) => TaskCompletionPopup( - goldEarned: GamificationService.calculateTaskGoldReward( - currentStats.stage, - ), taskName: task.title, + xpEarned: xpEarned, onClose: () => Navigator.of(context).pop(), ), ); diff --git a/lib/features/home/presentation/home_screen.dart b/lib/features/home/presentation/home_screen.dart index 862cdfb..5cbe650 100644 --- a/lib/features/home/presentation/home_screen.dart +++ b/lib/features/home/presentation/home_screen.dart @@ -4,7 +4,6 @@ import 'package:learning_coach/core/constants/app_strings.dart'; import 'package:learning_coach/core/providers/locale_provider.dart'; import 'package:learning_coach/features/garden/presentation/garden_screen.dart'; import 'package:learning_coach/features/home/presentation/widgets/home_widgets.dart'; -import 'package:learning_coach/features/shop/presentation/shop_screen.dart'; import 'package:learning_coach/shared/data/providers.dart'; import 'package:learning_coach/shared/models/gamification_models.dart'; import 'package:learning_coach/shared/services/gamification_service.dart'; @@ -55,8 +54,14 @@ class _HomeScreenState extends ConsumerState userStats.xp, userStats.level, ); - final stageName = GamificationService.getLocalizedStageName(userStats.stage, locale); - final powerName = GamificationService.getLocalizedPowerName(userStats.stage, locale); + final stageName = GamificationService.getLocalizedStageName( + userStats.stage, + locale, + ); + final powerName = GamificationService.getLocalizedPowerName( + userStats.stage, + locale, + ); final stageFeatures = GamificationService.getStageFeatures(userStats.stage); return Scaffold( @@ -81,99 +86,51 @@ class _HomeScreenState extends ConsumerState padding: const EdgeInsets.fromLTRB(20, 60, 20, 30), child: Column( children: [ - // Gold & Level Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Gold Display (Tappable to open shop) - GestureDetector( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const ShopScreen(), - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.95), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: const Color( - 0xFFFBBF24, - ).withOpacity(0.3), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - const Text( - '💰', - style: TextStyle(fontSize: 20), - ), - const SizedBox(width: 8), - Text( - '${userStats.gold}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFFF59E0B), - ), - ), - ], - ), - ), + // Level Badge Centered or Leading? + // Let's keep it clean. Just show Level Badge. + Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, ), - // Level Badge - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - stageFeatures.primaryColor, - stageFeatures.primaryColor.withOpacity(0.7), - ], - ), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: stageFeatures.primaryColor.withOpacity( - 0.3, - ), - blurRadius: 12, - offset: const Offset(0, 4), - ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + stageFeatures.primaryColor, + stageFeatures.primaryColor.withOpacity(0.8), ], ), - child: Row( - children: [ - Text( - stageFeatures.emoji, - style: const TextStyle(fontSize: 20), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: stageFeatures.primaryColor.withOpacity( + 0.3, ), - const SizedBox(width: 8), - Text( - 'Lv ${userStats.level}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + stageFeatures.emoji, + style: const TextStyle(fontSize: 24), + ), + const SizedBox(width: 12), + Text( + 'Lv ${userStats.level}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, ), - ], - ), + ), + ], ), - ], + ), ), const SizedBox(height: 30), @@ -274,28 +231,16 @@ class _HomeScreenState extends ConsumerState const SizedBox(height: 16), // Bonus Indicators - if (stageFeatures.xpBonus > 0 || - stageFeatures.goldBonus > 0) + if (stageFeatures.xpBonus > 0) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(16), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (stageFeatures.xpBonus > 0) - _buildBonusBadge( - '⚡ +${stageFeatures.xpBonus}% XP', - stageFeatures.primaryColor, - ), - if (stageFeatures.goldBonus > 0) - _buildBonusBadge( - '💰 +${stageFeatures.goldBonus}% ${AppStrings.getGoldBonus(locale)}', - const Color(0xFFF59E0B), - ), - ], + child: _buildBonusBadge( + '⚡ +${stageFeatures.xpBonus}% XP', + stageFeatures.primaryColor, ), ), @@ -349,10 +294,14 @@ class _HomeScreenState extends ConsumerState // Compact size for home screen preview const double containerSize = 150.0; - const double treeHeight = containerSize * 0.75; + + // Pot logic (Static base) const double potHeight = containerSize * 0.35; + const double potWidth = containerSize * 0.55; + + // Tree dimensions relative to asset aspect ratio (approx) + const double treeHeight = containerSize * 0.8; const double treeWidth = treeHeight * 0.85; - const double potWidth = treeWidth * 0.65; return SizedBox( width: containerSize, @@ -361,9 +310,9 @@ class _HomeScreenState extends ConsumerState alignment: Alignment.center, clipBehavior: Clip.none, children: [ - // Tree Layer (Behind) + // Tree Layer (Image Asset) Positioned( - bottom: containerSize * 0.32, // Tree base enters pot + bottom: containerSize * 0.28, // Just above pot bottom child: SizedBox( width: treeWidth, height: treeHeight, diff --git a/lib/features/home/presentation/widgets/home_widgets.dart b/lib/features/home/presentation/widgets/home_widgets.dart index 8f3ebcc..7674374 100644 --- a/lib/features/home/presentation/widgets/home_widgets.dart +++ b/lib/features/home/presentation/widgets/home_widgets.dart @@ -434,14 +434,6 @@ class ProgressSummaryCard extends ConsumerWidget { Icons.event_note_rounded, scheme, ), - _buildDivider(scheme), - _buildStatItem( - context, - '${data['averageScore'] ?? 0}%', - AppStrings.getAvgScore(locale), - Icons.star_rounded, - scheme, - ), ], ), ), diff --git a/lib/features/home/presentation/widgets/procedural_tree.dart b/lib/features/home/presentation/widgets/procedural_tree.dart new file mode 100644 index 0000000..efc4e68 --- /dev/null +++ b/lib/features/home/presentation/widgets/procedural_tree.dart @@ -0,0 +1,214 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class ProceduralTree extends StatelessWidget { + final int level; + final double size; + + const ProceduralTree({super.key, required this.level, required this.size}); + + @override + Widget build(BuildContext context) { + // Determine visuals based on level + // Cap visual evolution at level 10, but allow color changes after + final int visualLevel = level.clamp(1, 10); + final bool isMaxGrowth = level >= 10; + + // Trunk Color: Green (Lvl 1) -> Brown (Lvl 10) + final Color trunkColor = Color.lerp( + const Color(0xFF86EFAC), // Light Green + const Color(0xFF5D4037), // Dark Brown + (visualLevel - 1) / 9, + )!; + + // Leaf Color + Color leafColor = const Color(0xFF22C55E); // Base Green + if (level > 10) { + // Shift hue for levels > 10 + final double hue = ((level - 10) * 37.0) % 360; + leafColor = HSLColor.fromAHSL(1.0, hue, 0.7, 0.45).toColor(); + } + + // Fruit? + final bool showFruits = visualLevel >= 9; + + return CustomPaint( + size: Size(size, size), + painter: _TreePainter( + level: visualLevel, + isMaxGrowth: isMaxGrowth, + trunkColor: trunkColor, + leafColor: leafColor, + showFruits: showFruits, + ), + ); + } +} + +class _TreePainter extends CustomPainter { + final int level; + final bool isMaxGrowth; + final Color trunkColor; + final Color leafColor; + final bool showFruits; + + Random _rng = Random(42); // Seeded for consistency + + _TreePainter({ + required this.level, + required this.isMaxGrowth, + required this.trunkColor, + required this.leafColor, + required this.showFruits, + }); + + @override + void paint(Canvas canvas, Size size) { + _rng = Random(42); // Reset seed each paint + + final centerBottom = Offset(size.width / 2, size.height); + + // Properties based on level + // Height: Grows from 65% to 95% of container + final double treeHeight = size.height * (0.65 + (level / 10) * 0.3); + + // Trunk Thickness: Grows from 6 to 18 + final double trunkWidth = 6.0 + (level * 1.2); + + // Recursion Depth (Complexity) + final int depth = level <= 2 + ? 1 + : level <= 5 + ? 2 + : 3; + + // Draw Trunk & Branches + _drawBranch( + canvas, + centerBottom, + -pi / 2, // Upwards + treeHeight, + trunkWidth, + depth, + level, + ); + } + + void _drawBranch( + Canvas canvas, + Offset start, + double angle, + double length, + double width, + int currentDepth, + int currentLevel, + ) { + if (currentDepth < 0) return; + + final Paint trunkPaint = Paint() + ..color = trunkColor + ..style = PaintingStyle.stroke + ..strokeWidth = width + ..strokeCap = StrokeCap.round; + + // Calculate end point + final double endX = start.dx + cos(angle) * length; + final double endY = start.dy + sin(angle) * length; + final Offset end = Offset(endX, endY); + + // Quadratic Bezier for slight curve + // Curve intensity changes slightly pseudo-randomly but deterministically + final double curveFactor = + (_rng.nextDouble() - 0.5) * 20 * (4 - currentDepth); + final Offset control = Offset( + (start.dx + endX) / 2 + curveFactor, + (start.dy + endY) / 2, + ); + + final Path path = Path(); + path.moveTo(start.dx, start.dy); + path.quadraticBezierTo(control.dx, control.dy, endX, endY); + + canvas.drawPath(path, trunkPaint); + + // Draw Leaves/Fruits at ends or joints if complex enough + // Draw leaves on trunk too for bushier look at higher levels + if (currentDepth == 0 || (currentDepth == 1 && currentLevel >= 4)) { + _drawFoliage(canvas, end, width, currentLevel); + } + + // Recursive branches + if (currentDepth > 0) { + const int branches = 2; // Split into 2 + final double spreadAngle = + 0.6 + (currentLevel * 0.04).clamp(0.0, 0.4); // Wider spread + + _drawBranch( + canvas, + end, + angle - spreadAngle / 2, + length * 0.65, + width * 0.7, + currentDepth - 1, + currentLevel, + ); + + _drawBranch( + canvas, + end, + angle + spreadAngle / 2, + length * 0.65, + width * 0.7, + currentDepth - 1, + currentLevel, + ); + } + } + + void _drawFoliage( + Canvas canvas, + Offset center, + double stemWidth, + int currentLevel, + ) { + // Leaf Cluster + final Paint leafPaint = Paint() + ..color = leafColor + ..style = PaintingStyle.fill; + + // Cluster size grows with level + final double clusterRadius = 12.0 + (currentLevel * 1.8); + final int leafCount = 5 + currentLevel; // More density + + for (int i = 0; i < leafCount; i++) { + final double leafAngle = _rng.nextDouble() * 2 * pi; + final double distance = _rng.nextDouble() * clusterRadius; + + final double lx = center.dx + cos(leafAngle) * distance; + final double ly = center.dy + sin(leafAngle) * distance; + + // Draw individual leaf (Ellipse) + canvas.drawOval( + Rect.fromCenter(center: Offset(lx, ly), width: 8, height: 12), + leafPaint, + ); + } + + // Fruits + if (showFruits && _rng.nextDouble() > 0.6) { + final Paint fruitPaint = Paint()..color = const Color(0xFFEF4444); // Red + // Draw a fruit + canvas.drawCircle( + center + Offset(_rng.nextDouble() * 10 - 5, _rng.nextDouble() * 10 - 5), + 4.0, + fruitPaint, + ); + } + } + + @override + bool shouldRepaint(covariant _TreePainter oldDelegate) { + return oldDelegate.level != level || oldDelegate.leafColor != leafColor; + } +} diff --git a/lib/features/profile/presentation/profile_screen.dart b/lib/features/profile/presentation/profile_screen.dart index 51cea7c..72d93f1 100644 --- a/lib/features/profile/presentation/profile_screen.dart +++ b/lib/features/profile/presentation/profile_screen.dart @@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart'; import 'package:learning_coach/core/constants/app_strings.dart'; import 'package:learning_coach/core/providers/locale_provider.dart'; import 'package:learning_coach/features/auth/application/auth_controller.dart'; +import 'package:learning_coach/features/auth/domain/auth_state.dart'; +import 'package:learning_coach/features/auth/domain/auth_user.dart'; class ProfileScreen extends ConsumerStatefulWidget { const ProfileScreen({super.key}); @@ -174,31 +176,48 @@ class _ProfileScreenState extends ConsumerState { ), ), const SizedBox(height: 20), - Text( - 'Öğrenci Adı', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: -0.5, - ), - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - 'ogrenci@email.com', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white.withOpacity(0.95), - fontWeight: FontWeight.w500, - ), - ), + Consumer( + builder: (context, ref, child) { + final authState = ref.watch(authControllerProvider); + + final user = switch (authState) { + AuthStateAuthenticated(user: final u) => u, + _ => AuthUser.guest, + }; + + return Column( + children: [ + Text( + user.displayName, + style: Theme.of(context).textTheme.headlineSmall + ?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + user.email, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: Colors.white.withOpacity(0.95), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ); + }, ), ], ), diff --git a/lib/features/shop/presentation/shop_screen.dart b/lib/features/shop/presentation/shop_screen.dart index 3684703..e821ca5 100644 --- a/lib/features/shop/presentation/shop_screen.dart +++ b/lib/features/shop/presentation/shop_screen.dart @@ -26,35 +26,35 @@ class _ShopScreenState extends ConsumerState id: 'pot_terracotta', name: '🪴 Terracotta Saksı', category: ItemCategory.pot, - goldCost: 150, + goldCost: 0, assetPath: 'pot_terracotta', ), const InventoryItem( id: 'pot_ceramic', name: '🏺 Seramik Saksı', category: ItemCategory.pot, - goldCost: 250, + goldCost: 0, assetPath: 'pot_ceramic', ), const InventoryItem( id: 'pot_wooden', name: '🪵 Ahşap Saksı', category: ItemCategory.pot, - goldCost: 200, + goldCost: 0, assetPath: 'pot_wooden', ), const InventoryItem( id: 'pot_gold', name: '✨ Altın Saksı', category: ItemCategory.pot, - goldCost: 500, + goldCost: 0, assetPath: 'pot_gold', ), const InventoryItem( id: 'pot_crystal', name: '💎 Kristal Saksı', category: ItemCategory.pot, - goldCost: 600, + goldCost: 0, assetPath: 'pot_crystal', ), @@ -63,35 +63,35 @@ class _ShopScreenState extends ConsumerState id: 'bg_night', name: '🌙 Gece Gökyüzü', category: ItemCategory.background, - goldCost: 600, + goldCost: 0, assetPath: 'space_bg', ), const InventoryItem( id: 'bg_forest', name: '🌲 Orman Arka Planı', category: ItemCategory.background, - goldCost: 500, + goldCost: 0, assetPath: 'forest_bg', ), const InventoryItem( id: 'bg_ocean', name: '🌊 Okyanus Arka Planı', category: ItemCategory.background, - goldCost: 550, + goldCost: 0, assetPath: 'ocean_bg', ), const InventoryItem( id: 'bg_sunny', name: '☀️ Güneşli Park', category: ItemCategory.background, - goldCost: 400, + goldCost: 0, assetPath: 'sunny_bg', ), const InventoryItem( id: 'bg_rainbow', name: '🌈 Gökkuşağı', category: ItemCategory.background, - goldCost: 700, + goldCost: 0, assetPath: 'rainbow_bg', ), @@ -100,35 +100,35 @@ class _ShopScreenState extends ConsumerState id: 'comp_cat', name: '🐱 Uyuyan Kedi', category: ItemCategory.companion, - goldCost: 300, + goldCost: 0, assetPath: 'cat_companion', ), const InventoryItem( id: 'comp_bird', name: '🐦 Cıvıldayan Kuş', category: ItemCategory.companion, - goldCost: 250, + goldCost: 0, assetPath: 'bird_companion', ), const InventoryItem( id: 'comp_butterfly', name: '🦋 Kelebek', category: ItemCategory.companion, - goldCost: 200, + goldCost: 0, assetPath: 'butterfly_companion', ), const InventoryItem( id: 'comp_owl', name: '🦉 Bilge Baykuş', category: ItemCategory.companion, - goldCost: 400, + goldCost: 0, assetPath: 'owl_companion', ), const InventoryItem( id: 'comp_dragon', name: '🐉 Mini Ejderha', category: ItemCategory.companion, - goldCost: 800, + goldCost: 0, assetPath: 'dragon_companion', ), ]; @@ -192,41 +192,8 @@ class _ShopScreenState extends ConsumerState ?.copyWith(fontWeight: FontWeight.bold), ), const Spacer(), - // Gold Balance - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFFFBBF24), Color(0xFFF59E0B)], - ), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: const Color(0xFFF59E0B).withOpacity(0.3), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('💰', style: TextStyle(fontSize: 20)), - const SizedBox(width: 8), - Text( - '${userStats.gold}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ], - ), - ), + // Gold removed + const SizedBox.shrink(), ], ), ), @@ -572,27 +539,16 @@ class _ShopItemCard extends ConsumerWidget { vertical: 6, ), decoration: BoxDecoration( - color: canAfford - ? const Color(0xFFFBBF24).withOpacity(0.2) - : Colors.grey.withOpacity(0.2), + color: const Color(0xFF10B981).withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('💰', style: TextStyle(fontSize: 14)), - const SizedBox(width: 4), - Text( - '${item.goldCost}', - style: TextStyle( - color: canAfford - ? const Color(0xFFF59E0B) - : Colors.grey, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ], + child: const Text( + 'Ücretsiz', // Free + style: TextStyle( + color: Color(0xFF10B981), + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), ), ], @@ -627,19 +583,13 @@ class _PurchaseConfirmDialog extends ConsumerWidget { textAlign: TextAlign.center, ), const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Fiyat: 💰 ', style: TextStyle(fontSize: 18)), - Text( - '${item.goldCost}', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Color(0xFFF59E0B), - ), - ), - ], + const Text( + 'Ücretsiz', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFF10B981), + ), ), ], ), diff --git a/lib/features/study/presentation/session_running_screen.dart b/lib/features/study/presentation/session_running_screen.dart index 77aab25..1b90bc3 100644 --- a/lib/features/study/presentation/session_running_screen.dart +++ b/lib/features/study/presentation/session_running_screen.dart @@ -6,8 +6,8 @@ import 'package:go_router/go_router.dart'; import 'package:learning_coach/core/constants/app_strings.dart'; import 'package:learning_coach/core/providers/locale_provider.dart'; import 'package:learning_coach/shared/data/providers.dart'; +import 'package:learning_coach/shared/models/gamification_models.dart'; import 'package:learning_coach/shared/services/gamification_service.dart'; -import 'package:learning_coach/shared/widgets/avatar_character.dart'; import 'package:learning_coach/shared/widgets/reward_popups.dart'; class SessionRunningScreen extends ConsumerStatefulWidget { @@ -25,17 +25,31 @@ class SessionRunningScreen extends ConsumerStatefulWidget { _SessionRunningScreenState(); } -class _SessionRunningScreenState extends ConsumerState { +class _SessionRunningScreenState extends ConsumerState + with SingleTickerProviderStateMixin { // Timer late int _secondsRemaining; Timer? _timer; bool _isPaused = false; + // Animation + late AnimationController _swayController; + late Animation _swayAnimation; + @override void initState() { super.initState(); _secondsRemaining = widget.durationMinutes * 60; _startTimer(); + + _swayController = AnimationController( + duration: const Duration(seconds: 4), + vsync: this, + )..repeat(reverse: true); + + _swayAnimation = Tween(begin: -0.05, end: 0.05).animate( + CurvedAnimation(parent: _swayController, curve: Curves.easeInOut), + ); } void _startTimer() { @@ -58,6 +72,7 @@ class _SessionRunningScreenState extends ConsumerState { @override void dispose() { _timer?.cancel(); + _swayController.dispose(); super.dispose(); } @@ -127,12 +142,8 @@ class _SessionRunningScreenState extends ConsumerState { color: scheme.primary, ), ), - // Animated Avatar in Training Pose - AvatarCharacter( - stage: userStats.stage.name, - size: 180, - isAnimating: !_isPaused, - ), + // Plant with Swaying Animation + _buildSwayingPlant(userStats, scheme), ], ), const SizedBox(height: 32), @@ -209,20 +220,57 @@ class _SessionRunningScreenState extends ConsumerState { ); } - void _finishSession() async { + void _finishSession() { _timer?.cancel(); - // Calculate study time based on elapsed time (not just full duration if finished early) - // Assuming button press finishes session with current elapsed time? - // User requested: "bu hedefin ilerlemesini ne kadar süre çalıştığını db'ye kaydedilsin" - // Usually "finish" means "done for now". - final elapsedSeconds = (widget.durationMinutes * 60) - _secondsRemaining; final elapsedMinutes = elapsedSeconds ~/ 60; + final currentStats = ref.read(userStatsProvider); - // Minimum 1 minutes to record? or just record whatever. - // Let's record. + // Minimum 10 minutes required for rewards + if (elapsedMinutes < 10) { + // Show VictoryPopup in "Early Exit" mode (0 XP) + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => VictoryPopup( + xpEarned: 0, + newLevel: currentStats.level, + leveledUp: false, + onContinue: () { + // "Bitir" pressed + Navigator.of(context).pop(); // Close dialog + _completeSession( + elapsedSeconds, + elapsedMinutes, + showSuccessDialog: false, + ); + if (mounted && context.canPop()) { + context.pop(); // Close screen + } + }, + onResume: () { + // "Devam Et" pressed + Navigator.of(context).pop(); // Close dialog + // Timer is already paused by logic flow? No, _finishSession cancels it. + // We need to decide if we restart it or if we just let it be. + // _finishSession cancelled it. We should probably restart it or not cancel it until confirmed. + // Wait, the previous logic cancelled timer at start of _finishSession. + // If we resume, we need to restart timer. + _startTimer(); + }, + ), + ); + } else { + _completeSession(elapsedSeconds, elapsedMinutes); + } + } + Future _completeSession( + int elapsedSeconds, + int elapsedMinutes, { + bool showSuccessDialog = true, + }) async { try { await ref .read(apiStudySessionRepositoryProvider) @@ -236,34 +284,31 @@ class _SessionRunningScreenState extends ConsumerState { final currentStats = ref.read(userStatsProvider); final newStats = GamificationService.awardStudyRewards( currentStats, - elapsedMinutes > 0 - ? elapsedMinutes - : 1, // Minimum 1 minute for reward calc + elapsedMinutes, // Corrected to use minutes ); final leveledUp = newStats.level > currentStats.level; // Update stats ref.read(userStatsProvider.notifier).updateStats(newStats); - // Show victory popup - if (mounted) { + // Show victory popup if requested (and if we have rewards or it's a normal finish) + // Actually strictly respecting the flag is safer for the double-dialog issue. + if (showSuccessDialog && mounted) { await showDialog( context: context, barrierDismissible: false, builder: (context) => VictoryPopup( xpEarned: GamificationService.calculateXpReward( - elapsedMinutes > 0 ? elapsedMinutes : 1, - currentStats.stage, - ), - goldEarned: GamificationService.calculateSessionGoldReward( + elapsedMinutes, currentStats.stage, ), newLevel: newStats.level, leveledUp: leveledUp, onContinue: () { Navigator.of(context).pop(); // Close dialog - context.pop(); // Close running screen, return to study menu + context.pop(); // Close running screen }, + // onResume not needed here ), ); } @@ -275,4 +320,125 @@ class _SessionRunningScreenState extends ConsumerState { } } } + + Widget _buildSwayingPlant(UserStats userStats, ColorScheme scheme) { + final treeAsset = GamificationService.getTreeAssetPath(userStats.level); + const double containerSize = 220.0; + + // Pot logic (Static base) + const double potHeight = containerSize * 0.35; + const double potWidth = containerSize * 0.55; + + // Tree dimensions + const double treeHeight = containerSize * 0.8; + const double treeWidth = treeHeight * 0.85; + + return SizedBox( + width: containerSize, + height: containerSize, + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + // Tree Layer (Animated) + Positioned( + bottom: containerSize * 0.28, + child: AnimatedBuilder( + animation: _swayAnimation, + builder: (context, child) { + return Transform.rotate( + angle: _isPaused ? 0 : _swayAnimation.value, + alignment: Alignment.bottomCenter, + child: SizedBox( + width: treeWidth, + height: treeHeight, + child: Image.asset( + treeAsset, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.park, + size: 80, + color: Colors.green, + ); + }, + ), + ), + ); + }, + ), + ), + + // Pot Layer (Static Front) + Positioned( + bottom: containerSize * 0.25, + child: Container( + width: potWidth, + height: potHeight, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.brown[400]!, Colors.brown[700]!], + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Stack( + children: [ + // Pot rim + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: potHeight * 0.2, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.brown[300]!, Colors.brown[500]!], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + ), + ), + // Soil or Label area + Positioned( + top: potHeight * 0.3, + left: potWidth * 0.2, + right: potWidth * 0.2, + child: Container( + height: potHeight * 0.4, + decoration: BoxDecoration( + color: Colors.brown[900]?.withOpacity(0.3), + borderRadius: BorderRadius.circular(4), + ), + child: Center( + child: Icon( + Icons.eco, + color: Colors.green[200]!.withOpacity(0.5), + size: 16, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } } diff --git a/lib/shared/data/api_stats_repository.dart b/lib/shared/data/api_stats_repository.dart index 2d117c9..5900db7 100644 --- a/lib/shared/data/api_stats_repository.dart +++ b/lib/shared/data/api_stats_repository.dart @@ -40,6 +40,22 @@ class ApiStatsRepository { } } + Future updateUserStats(UserStats stats) async { + try { + await _dio.post( + '/user/stats', + data: { + 'current_level': stats.currentLevel, + 'current_xp': stats.currentXP, + 'total_gold': stats.totalGold, + 'avatar_stage': stats.stage.name, // Enum to string + }, + ); + } catch (e) { + rethrow; + } + } + Future> getDailyStats() async { try { final response = await _dio.get>('/user/daily'); diff --git a/lib/shared/data/providers.dart b/lib/shared/data/providers.dart index 69fef51..475c800 100644 --- a/lib/shared/data/providers.dart +++ b/lib/shared/data/providers.dart @@ -171,6 +171,16 @@ class UserStatsNotifier extends _$UserStatsNotifier { } } + Future _persistStats(UserStats newStats) async { + state = newStats; + try { + await ref.read(apiStatsRepositoryProvider).updateUserStats(newStats); + } catch (e) { + print('Error persisting stats: $e'); + // Optionally revert state or show error + } + } + void addXP(int amount) { if (amount <= 0) return; @@ -178,16 +188,18 @@ class UserStatsNotifier extends _$UserStatsNotifier { int newLevel = GamificationService.calculateLevel(newXP); final newStage = GamificationService.getAvatarStage(newLevel); - state = state.copyWith( + final newStats = state.copyWith( currentXP: newXP, currentLevel: newLevel, stage: newStage, ); + _persistStats(newStats); } void addGold(int amount) { if (amount <= 0) return; - state = state.copyWith(totalGold: state.totalGold + amount); + final newStats = state.copyWith(totalGold: state.totalGold + amount); + _persistStats(newStats); } String getCharacterAssetPath() { @@ -195,6 +207,8 @@ class UserStatsNotifier extends _$UserStatsNotifier { } void awardStudyRewards(int studyMinutes) { + // This is likely not used if we use updateStats directly from screen + // But keeping it consistent final xpGained = GamificationService.calculateXpReward( studyMinutes, state.stage, @@ -202,21 +216,48 @@ class UserStatsNotifier extends _$UserStatsNotifier { final goldGained = GamificationService.calculateSessionGoldReward( state.stage, ); - addXP(xpGained); - addGold(goldGained); + + // Calculate new state logic duplicated from addXP/addGold but combined + int newXP = state.currentXP + xpGained; + int newLevel = GamificationService.calculateLevel(newXP); + final newStage = GamificationService.getAvatarStage(newLevel); + int newGold = state.totalGold + goldGained; + + final newStats = state.copyWith( + currentXP: newXP, + currentLevel: newLevel, + stage: newStage, + totalGold: newGold, + ); + _persistStats(newStats); } void awardTaskRewards() { + final xpGained = GamificationService.calculateTaskXpReward(state.stage); final goldGained = GamificationService.calculateTaskGoldReward(state.stage); - addGold(goldGained); + + // Calculate new state logic + int newXP = state.currentXP + xpGained; + int newLevel = GamificationService.calculateLevel(newXP); + final newStage = GamificationService.getAvatarStage(newLevel); + int newGold = state.totalGold + goldGained; + + final newStats = state.copyWith( + currentXP: newXP, + currentLevel: newLevel, + stage: newStage, + totalGold: newGold, + ); + _persistStats(newStats); } void purchaseItem(String itemId, int cost) { if (state.totalGold >= cost && !state.purchasedItemIds.contains(itemId)) { - state = state.copyWith( + final newStats = state.copyWith( totalGold: state.totalGold - cost, purchasedItemIds: [...state.purchasedItemIds, itemId], ); + _persistStats(newStats); } } @@ -224,17 +265,19 @@ class UserStatsNotifier extends _$UserStatsNotifier { if (state.purchasedItemIds.contains(itemId)) { final updated = Map.from(state.equippedItems); updated[category] = itemId; - state = state.copyWith(equippedItems: updated); + final newStats = state.copyWith(equippedItems: updated); + _persistStats(newStats); } } void unequipItem(String category) { final updated = Map.from(state.equippedItems); updated.remove(category); - state = state.copyWith(equippedItems: updated); + final newStats = state.copyWith(equippedItems: updated); + _persistStats(newStats); } void updateStats(UserStats newStats) { - state = newStats; + _persistStats(newStats); } } diff --git a/lib/shared/services/gamification_service.dart b/lib/shared/services/gamification_service.dart index 79f341c..bfbd229 100644 --- a/lib/shared/services/gamification_service.dart +++ b/lib/shared/services/gamification_service.dart @@ -11,7 +11,7 @@ class GamificationService { static const double xpMultiplier = 1.5; // --- Base Reward Constants --- - static const int baseXpPerMinute = 10; + static const int baseXpPerTask = 5; static const int baseGoldPerTask = 50; static const int baseGoldPerSession = 25; @@ -21,18 +21,15 @@ class GamificationService { static const int bloomMaxLevel = 25; static const int treeMaxLevel = 35; - /// Calculate XP with stage bonus + /// Calculate XP with stage bonus - DEPRECATED BONUS static int calculateXpWithBonus(int baseXp, AvatarStage stage) { - final features = getStageFeatures(stage); - final bonusXp = (baseXp * features.xpBonus / 100).round(); - return baseXp + bonusXp; + // Bonuses removed for linear progression + return baseXp; } - /// Calculate Gold with stage bonus + /// Calculate Gold with stage bonus - DEPRECATED static int calculateGoldWithBonus(int baseGold, AvatarStage stage) { - final features = getStageFeatures(stage); - final bonusGold = (baseGold * features.goldBonus / 100).round(); - return baseGold + bonusGold; + return 0; // Gold removed } /// Calculate level from total XP @@ -42,20 +39,28 @@ class GamificationService { return (sqrt(totalXp / baseXpPerLevel)).floor() + 1; } - /// Get XP reward for study with stage bonus + /// Get XP reward for study + /// Rules: + /// - 1 min = 1 XP + /// - Minimum 10 minutes required static int calculateXpReward(int studyMinutes, AvatarStage stage) { - final baseXp = studyMinutes * baseXpPerMinute; - return calculateXpWithBonus(baseXp, stage); + if (studyMinutes < 10) return 0; + return studyMinutes; + } + + /// Get XP reward for task + static int calculateTaskXpReward(AvatarStage stage) { + return baseXpPerTask; } - /// Get Gold reward for task with stage bonus + /// Get Gold reward for task - DEPRECATED static int calculateTaskGoldReward(AvatarStage stage) { - return calculateGoldWithBonus(baseGoldPerTask, stage); + return 0; } - /// Get Gold reward for session with stage bonus + /// Get Gold reward for session - DEPRECATED static int calculateSessionGoldReward(AvatarStage stage) { - return calculateGoldWithBonus(baseGoldPerSession, stage); + return 0; } /// Calculate XP required for next level @@ -106,17 +111,19 @@ class GamificationService { } } + /// Get tree asset path based on level (for Garden Screen) /// Get tree asset path based on level (for Garden Screen) static String getTreeAssetPath(int level) { - if (level >= 40) return 'assets/images/tree_lvl5.png'; // Ancient Tree - if (level >= 30) return 'assets/images/tree_lvl4.png'; // Mature Tree - if (level >= 20) return 'assets/images/tree_lvl3.png'; // Young Tree - if (level >= 10) return 'assets/images/tree_lvl2.png'; // Sapling + if (level >= 20) return 'assets/images/tree_lvl5.png'; // Ancient Tree + if (level >= 15) return 'assets/images/tree_lvl4.png'; // Mature Tree + if (level >= 10) return 'assets/images/tree_lvl3.png'; // Young Tree + if (level >= 5) return 'assets/images/tree_lvl2.png'; // Sapling return 'assets/images/tree_lvl1.png'; // Seed/Sprout } /// Get stage features - Each stage has unique bonuses static StageFeatures getStageFeatures(AvatarStage stage) { + // Bonuses are set to 0 to support consistent 1 XP/min logic switch (stage) { case AvatarStage.seed: return const StageFeatures( @@ -134,8 +141,8 @@ class GamificationService { title: '🌿 Filiz', description: 'Büyümeye başladın! Artık daha hızlı öğreniyorsun.', powerName: 'Hızlı Büyüme', - xpBonus: 10, // +10% XP - goldBonus: 5, // +5% Gold + xpBonus: 0, // Previously 10 + goldBonus: 0, // Previously 5 emoji: '🌿', primaryColor: Color(0xFF10B981), secondaryColor: Color(0xFFD1FAE5), @@ -145,8 +152,8 @@ class GamificationService { title: '🌸 Çiçek', description: 'Tüm potansiyelini açığa çıkarıyorsun!', powerName: 'Çiçek Açımı', - xpBonus: 25, // +25% XP - goldBonus: 15, // +15% Gold + xpBonus: 0, // Previously 25 + goldBonus: 0, // Previously 15 emoji: '🌸', primaryColor: Color(0xFFEC4899), secondaryColor: Color(0xFFFCE7F3), @@ -156,8 +163,8 @@ class GamificationService { title: '🌳 Ağaç', description: 'Güçlü ve köklü bir bilgesin artık.', powerName: 'Bilgelik Ağacı', - xpBonus: 40, // +40% XP - goldBonus: 30, // +30% Gold + xpBonus: 0, // Previously 40 + goldBonus: 0, // Previously 30 emoji: '🌳', primaryColor: Color(0xFF059669), secondaryColor: Color(0xFFA7F3D0), @@ -167,8 +174,8 @@ class GamificationService { title: '🌲 Orman', description: 'Efsanevi usta! Senin bilgin tüm ormanı besliyor.', powerName: 'Usta Ormanı', - xpBonus: 60, // +60% XP - goldBonus: 50, // +50% Gold + xpBonus: 0, // Previously 60 + goldBonus: 0, // Previously 50 emoji: '🌲', primaryColor: Color(0xFF0369A1), secondaryColor: Color(0xFFBAE6FD), @@ -249,10 +256,20 @@ class GamificationService { /// Update user stats after task completion static UserStats awardTaskRewards(UserStats currentStats) { + final xpGained = calculateTaskXpReward(currentStats.stage); final goldGained = calculateTaskGoldReward(currentStats.stage); + + final newXp = currentStats.currentXP + xpGained; final newGold = currentStats.totalGold + goldGained; + final newLevel = calculateLevel(newXp); + final newStage = getAvatarStage(newLevel); - return currentStats.copyWith(totalGold: newGold); + return currentStats.copyWith( + currentXP: newXp, + totalGold: newGold, + currentLevel: newLevel, + stage: newStage, + ); } /// Purchase item with gold diff --git a/lib/shared/widgets/reward_popups.dart b/lib/shared/widgets/reward_popups.dart index b723fac..5a18736 100644 --- a/lib/shared/widgets/reward_popups.dart +++ b/lib/shared/widgets/reward_popups.dart @@ -1,22 +1,24 @@ -import 'package:flutter/material.dart'; -import 'package:confetti/confetti.dart'; import 'dart:math' as math; +import 'package:confetti/confetti.dart'; +import 'package:flutter/material.dart'; + /// Victory Popup - Shows XP and Gold rewards after study session class VictoryPopup extends StatefulWidget { final int xpEarned; - final int goldEarned; + // Gold removed final int newLevel; final bool leveledUp; final VoidCallback onContinue; + final VoidCallback? onResume; const VictoryPopup({ super.key, required this.xpEarned, - required this.goldEarned, required this.newLevel, this.leveledUp = false, required this.onContinue, + this.onResume, }); @override @@ -43,17 +45,18 @@ class _VictoryPopupState extends State curve: Curves.elasticOut, ); - _fadeAnimation = CurvedAnimation( - parent: _controller, - curve: Curves.easeIn, - ); + _fadeAnimation = CurvedAnimation(parent: _controller, curve: Curves.easeIn); _confettiController = ConfettiController( duration: const Duration(seconds: 3), ); _controller.forward(); - _confettiController.play(); + + // Only play confetti if XP was earned + if (widget.xpEarned > 0) { + _confettiController.play(); + } } @override @@ -65,26 +68,29 @@ class _VictoryPopupState extends State @override Widget build(BuildContext context) { + final hasEarnedRewards = widget.xpEarned > 0; + return Stack( children: [ - // Confetti - Align( - alignment: Alignment.topCenter, - child: ConfettiWidget( - confettiController: _confettiController, - blastDirectionality: BlastDirectionality.explosive, - shouldLoop: false, - colors: const [ - Color(0xFF6366F1), - Color(0xFFEC4899), - Color(0xFF10B981), - Color(0xFFF59E0B), - Color(0xFF8B5CF6), - ], - numberOfParticles: 30, - gravity: 0.3, + // Confetti (Only if rewards earned) + if (hasEarnedRewards) + Align( + alignment: Alignment.topCenter, + child: ConfettiWidget( + confettiController: _confettiController, + blastDirectionality: BlastDirectionality.explosive, + shouldLoop: false, + colors: const [ + Color(0xFF6366F1), + Color(0xFFEC4899), + Color(0xFF10B981), + Color(0xFFF59E0B), + Color(0xFF8B5CF6), + ], + numberOfParticles: 30, + gravity: 0.3, + ), ), - ), // Dialog Center( child: ScaleTransition( @@ -96,19 +102,25 @@ class _VictoryPopupState extends State child: Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - Color(0xFF6366F1), - Color(0xFF8B5CF6), - Color(0xFF9333EA), - ], + gradient: LinearGradient( + colors: hasEarnedRewards + ? [ + const Color(0xFF6366F1), + const Color(0xFF8B5CF6), + const Color(0xFF9333EA), + ] + : [const Color(0xFF64748B), const Color(0xFF94A3B8)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(32), boxShadow: [ BoxShadow( - color: const Color(0xFF6366F1).withOpacity(0.5), + color: + (hasEarnedRewards + ? const Color(0xFF6366F1) + : Colors.black) + .withOpacity(0.5), blurRadius: 40, offset: const Offset(0, 20), ), @@ -117,7 +129,7 @@ class _VictoryPopupState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Trophy Icon + // Icon TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 800), @@ -130,8 +142,10 @@ class _VictoryPopupState extends State color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), - child: const Icon( - Icons.emoji_events_rounded, + child: Icon( + hasEarnedRewards + ? Icons.emoji_events_rounded + : Icons.access_time_filled_rounded, size: 64, color: Colors.white, ), @@ -142,7 +156,12 @@ class _VictoryPopupState extends State const SizedBox(height: 24), // Title Text( - widget.leveledUp ? '🎉 Seviye Atladın!' : '⚔️ Zafer!', + // If 0 XP (duration < 10 mins), show specific message + !hasEarnedRewards + ? 'Henüz Erken!' + : widget.leveledUp + ? '🎉 Seviye Atladın!' + : '⚔️ Zafer!', style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, @@ -152,55 +171,103 @@ class _VictoryPopupState extends State textAlign: TextAlign.center, ), const SizedBox(height: 8), - if (widget.leveledUp) - Text( - 'Seviye ${widget.newLevel}', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Colors.white.withOpacity(0.9), - ), + // Subtitle / Description + Text( + !hasEarnedRewards + ? 'XP kazanmak için minimum 10 dakika çalışmalısın.' + : widget.leveledUp + ? 'Seviye ${widget.newLevel}' + : 'Harika bir çalışma seansıydı!', + style: TextStyle( + fontSize: !hasEarnedRewards ? 16 : 20, + fontWeight: FontWeight.w600, + color: Colors.white.withOpacity(0.9), + height: 1.5, ), - const SizedBox(height: 32), - // Rewards - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildRewardItem( - '⭐', - '${widget.xpEarned} XP', - ), - _buildRewardItem( - '💰', - '${widget.goldEarned} Altın', - ), - ], + textAlign: TextAlign.center, ), const SizedBox(height: 32), - // Continue Button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: widget.onContinue, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: const Color(0xFF6366F1), - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + // Rewards (Only if earned) + if (hasEarnedRewards) + Center( + child: _buildRewardItem('⭐', '${widget.xpEarned} XP'), + ), + if (hasEarnedRewards) const SizedBox(height: 32), + + // Actions + if (!hasEarnedRewards) ...[ + Row( + children: [ + Expanded( + child: TextButton( + onPressed: + widget.onContinue, // Actually "Finish" here + style: TextButton.styleFrom( + foregroundColor: Colors.white70, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + child: const Text( + 'Bitir', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), ), - elevation: 0, - ), - child: const Text( - 'Devam Et', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: widget.onResume, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF64748B), + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + child: const Text( + 'Devam Et', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ] else ...[ + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: widget.onContinue, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF6366F1), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + child: const Text( + 'Devam Et', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), ), ), ), - ), + ], ], ), ), @@ -229,10 +296,7 @@ class _VictoryPopupState extends State ), child: Column( children: [ - Text( - emoji, - style: const TextStyle(fontSize: 32), - ), + Text(emoji, style: const TextStyle(fontSize: 32)), const SizedBox(height: 8), Text( text, @@ -254,14 +318,14 @@ class _VictoryPopupState extends State /// Task Completion Popup - Shows gold reward class TaskCompletionPopup extends StatelessWidget { - final int goldEarned; final String taskName; + final int xpEarned; final VoidCallback onClose; const TaskCompletionPopup({ super.key, - required this.goldEarned, required this.taskName, + this.xpEarned = 0, required this.onClose, }); @@ -317,29 +381,34 @@ class TaskCompletionPopup extends StatelessWidget { ), textAlign: TextAlign.center, ), - const SizedBox(height: 24), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(16), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('💰', style: TextStyle(fontSize: 28)), - const SizedBox(width: 12), - Text( - '+$goldEarned Altın', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, + if (xpEarned > 0) ...[ + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('⭐', style: TextStyle(fontSize: 20)), + const SizedBox(width: 8), + Text( + '+$xpEarned XP', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), - ), - ], + ], + ), ), - ), + ], const SizedBox(height: 24), SizedBox( width: double.infinity, diff --git a/pubspec.lock b/pubspec.lock index 20492aa..66a4067 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -492,10 +492,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" url: "https://pub.dev" source: hosted - version: "0.8.13+6" + version: "0.8.13+3" image_picker_linux: dependency: transitive description: @@ -628,10 +628,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -985,26 +985,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.11" timing: dependency: transitive description: @@ -1118,5 +1118,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.0"