import 'package:flutter/material.dart'; import 'package:practice_engine/practice_engine.dart'; import '../routes.dart'; import '../services/deck_storage.dart'; class DeckOverviewScreen extends StatefulWidget { const DeckOverviewScreen({super.key}); @override State createState() => _DeckOverviewScreenState(); } class _DeckOverviewScreenState extends State { Deck? _deck; final DeckStorage _deckStorage = DeckStorage(); @override void didChangeDependencies() { super.didChangeDependencies(); // Get deck from route arguments final args = ModalRoute.of(context)?.settings.arguments; if (args is Deck) { // ALWAYS load the latest version from storage to ensure we have the most up-to-date deck // This is critical for getting the latest incomplete attempt state // Don't rely on route arguments - they might be stale final storedDeck = _deckStorage.getDeckSync(args.id); final deckToUse = storedDeck ?? args; // Always update to get the latest state from storage // This ensures we always have the most up-to-date incomplete attempt state setState(() { _deck = deckToUse; }); } } void _saveDeck() { if (_deck != null) { _deckStorage.saveDeckSync(_deck!); } } @override void initState() { super.initState(); } void _startAttempt() async { // Force reload from storage before checking for incomplete attempts to ensure we have latest state Deck? deckToCheck = _deck; if (_deck != null) { final freshDeck = _deckStorage.getDeckSync(_deck!.id); if (freshDeck != null) { setState(() { _deck = freshDeck; }); deckToCheck = freshDeck; // Use the fresh deck for the check } } if (deckToCheck == null || deckToCheck.questions.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Cannot start attempt: No questions in deck'), backgroundColor: Colors.red, ), ); return; } // Check for incomplete attempt first - use the fresh deck we just loaded if (deckToCheck.incompleteAttempt != null) { final incomplete = deckToCheck.incompleteAttempt!; final progress = incomplete.currentQuestionIndex + 1; final total = incomplete.questionIds.length; final action = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Incomplete Attempt Found'), content: Text( 'You have an incomplete attempt (Question $progress of $total).\n\n' 'Would you like to continue where you left off, or start a new attempt?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, 'ignore'), child: const Text('Start Fresh'), ), FilledButton( onPressed: () => Navigator.pop(context, 'continue'), child: const Text('Continue'), ), ], ), ); if (action == 'continue') { // Continue the incomplete attempt if (!mounted) return; Navigator.pushNamed( context, Routes.attempt, arguments: { 'deck': deckToCheck, 'resumeAttempt': true, }, ).then((_) { // Refresh deck state when returning from attempt _refreshDeck(); }); return; } else if (action == 'ignore') { // Clear incomplete attempt and start fresh // Clear incomplete attempt - create a fresh copy without it final updatedDeck = deckToCheck.copyWith(clearIncompleteAttempt: true); // Save to storage multiple times to ensure it's persisted _deckStorage.saveDeckSync(updatedDeck); _deckStorage.saveDeckSync(updatedDeck); // Save twice to be sure // Update local state immediately with cleared deck setState(() { _deck = updatedDeck; }); // Force reload from storage multiple times to verify it's cleared Deck? finalClearedDeck = updatedDeck; for (int i = 0; i < 3; i++) { final verifiedDeck = _deckStorage.getDeckSync(updatedDeck.id); if (verifiedDeck != null) { // Ensure incomplete attempt is null even if storage had it if (verifiedDeck.incompleteAttempt != null) { final clearedDeck = verifiedDeck.copyWith(clearIncompleteAttempt: true); _deckStorage.saveDeckSync(clearedDeck); setState(() { _deck = clearedDeck; }); finalClearedDeck = clearedDeck; } else { // Already cleared, update state setState(() { _deck = verifiedDeck; }); finalClearedDeck = verifiedDeck; break; // Exit loop if already cleared } } } // CRITICAL: Update deckToCheck to use the cleared deck for the rest of the flow deckToCheck = finalClearedDeck ?? updatedDeck; // Continue to normal flow below - the deck should now have no incomplete attempt } else { // User cancelled return; } } // Check if all questions are known and includeKnownInAttempts is disabled final allKnown = deckToCheck.knownCount == deckToCheck.numberOfQuestions && deckToCheck.numberOfQuestions > 0; final includeKnownDisabled = !deckToCheck.config.includeKnownInAttempts; if (allKnown && includeKnownDisabled) { // Show confirmation dialog final shouldIncludeKnown = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('All Questions Known'), content: const Text( 'All questions are known, attempt with known questions?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('No'), ), FilledButton( onPressed: () => Navigator.pop(context, true), child: const Text('Yes'), ), ], ), ); if (shouldIncludeKnown == null || !shouldIncludeKnown) { // User cancelled or said No return; } // Pass the deck with a flag to include known questions // Ensure we're using a deck without incomplete attempt if (!mounted) return; final deckToUse = deckToCheck.copyWith(clearIncompleteAttempt: true); Navigator.pushNamed( context, Routes.attempt, arguments: { 'deck': deckToUse, 'includeKnown': true, }, ).then((_) { // Refresh deck state when returning from attempt _refreshDeck(); }); } else { // Normal flow - ensure we're using a deck without incomplete attempt if (!mounted) return; // Always use a fresh deck copy without incomplete attempt to prevent stale state // Also ensure storage has the cleared state final deckToUse = deckToCheck.copyWith(clearIncompleteAttempt: true); // Ensure storage also has the cleared state _deckStorage.saveDeckSync(deckToUse); Navigator.pushNamed( context, Routes.attempt, arguments: deckToUse, ).then((_) { // Refresh deck state when returning from attempt _refreshDeck(); }); } } void _refreshDeck() { if (_deck != null) { // Reload the deck from storage to get the latest state (including incomplete attempts) final refreshedDeck = _deckStorage.getDeckSync(_deck!.id); if (refreshedDeck != null && mounted) { setState(() { _deck = refreshedDeck; }); } } } void _openConfig() { if (_deck == null) return; Navigator.pushNamed(context, Routes.deckConfig, arguments: _deck) .then((updatedDeck) { if (updatedDeck != null && updatedDeck is Deck) { final wasReset = updatedDeck.attemptHistory.isEmpty && updatedDeck.currentAttemptIndex == 0 && _deck != null && _deck!.attemptHistory.isNotEmpty; setState(() { _deck = updatedDeck; }); _saveDeck(); // Save to storage // Show confirmation message if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(wasReset ? 'Deck has been reset successfully' : 'Deck settings have been updated'), backgroundColor: Colors.green, duration: const Duration(seconds: 2), ), ); } } }); } String _formatTime(int milliseconds) { final seconds = milliseconds ~/ 1000; final minutes = seconds ~/ 60; final hours = minutes ~/ 60; final remainingMinutes = minutes % 60; final remainingSeconds = seconds % 60; if (hours > 0) { return '${hours}h ${remainingMinutes}m'; } else if (minutes > 0) { return '${minutes}m ${remainingSeconds}s'; } else { return '${remainingSeconds}s'; } } Widget _buildProgressStats(BuildContext context) { if (_deck == null) return const SizedBox.shrink(); final totalQuestions = _deck!.numberOfQuestions; final knownCount = _deck!.knownCount; final unknownCount = totalQuestions - knownCount; final unknownPercentage = totalQuestions > 0 ? (unknownCount / totalQuestions * 100.0) : 0.0; final knownPercentage = totalQuestions > 0 ? (knownCount / totalQuestions * 100.0) : 0.0; // Get progress trend if we have history // Attempt history is stored chronologically (oldest first), so first is oldest, last is newest String? trendText; if (_deck!.attemptHistory.isNotEmpty) { final oldestAttempt = _deck!.attemptHistory.first; final newestAttempt = _deck!.attemptHistory.last; final oldestUnknown = oldestAttempt.unknownPercentage; final newestUnknown = newestAttempt.unknownPercentage; final improvement = oldestUnknown - newestUnknown; if (improvement > 0) { trendText = 'Improved by ${improvement.toStringAsFixed(1)}% since first attempt'; } else if (improvement < 0) { trendText = '${improvement.abs().toStringAsFixed(1)}% more to learn since first attempt'; } else { trendText = 'No change since first attempt'; } } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Main stat: Unknown percentage Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Questions to Learn', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), ), ), const SizedBox(height: 4), Text( '${unknownPercentage.toStringAsFixed(1)}%', style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), Text( '$unknownCount of $totalQuestions questions', style: Theme.of(context).textTheme.bodySmall, ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( 'Questions Known', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), ), ), const SizedBox(height: 4), Text( '${knownPercentage.toStringAsFixed(1)}%', style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.secondary, ), ), Text( '$knownCount of $totalQuestions questions', style: Theme.of(context).textTheme.bodySmall, ), ], ), ], ), const SizedBox(height: 16), // Progress bar Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Progress', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), ), ), Text( '${knownPercentage.toStringAsFixed(1)}%', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(8), child: LinearProgressIndicator( value: knownPercentage / 100.0, minHeight: 12, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), ), ], ), if (trendText != null) ...[ const SizedBox(height: 12), Text( trendText, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ], ); } @override Widget build(BuildContext context) { if (_deck == null) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } return Scaffold( appBar: AppBar( title: Text(_deck!.title), ), body: SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Deck Description Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Description', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 4), Text( _deck!.description, style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), const SizedBox(height: 12), // Practice Progress Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ SizedBox( width: 80, height: 80, child: CircularProgressIndicator( value: _deck!.practicePercentage / 100, strokeWidth: 6, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${_deck!.practicePercentage.toStringAsFixed(1)}%', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '${_deck!.knownCount} of ${_deck!.numberOfQuestions} questions known', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ], ), ), ), const SizedBox(height: 12), // Statistics Card( child: Padding( padding: const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _StatItem( label: 'Total', value: '${_deck!.numberOfQuestions}', icon: Icons.quiz, ), _StatItem( label: 'Known', value: '${_deck!.knownCount}', icon: Icons.check_circle, ), _StatItem( label: 'Practice', value: '${_deck!.numberOfQuestions - _deck!.knownCount}', icon: Icons.school, ), ], ), ), ), const SizedBox(height: 12), // Attempts History Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.history, size: 20, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Text( 'Attempts History', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 16), if (_deck!.attemptHistory.isEmpty) Padding( padding: const EdgeInsets.all(16), child: Center( child: Column( children: [ Icon( Icons.quiz_outlined, size: 48, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), ), const SizedBox(height: 8), Text( 'No attempts yet', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ), ), ) else ...[ // Summary Statistics Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _HistoryStatItem( label: 'Attempts', value: '${_deck!.attemptCount}', icon: Icons.quiz, ), _HistoryStatItem( label: 'Avg. Score', value: '${_deck!.averagePercentageCorrect.toStringAsFixed(1)}%', icon: Icons.trending_up, ), _HistoryStatItem( label: 'Total Time', value: _formatTime(_deck!.totalTimeSpent), icon: Icons.timer, ), ], ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 8), Text( 'Learning Progress', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 12), // Current progress _buildProgressStats(context), ], ], ), ), ), const SizedBox(height: 12), // Action Buttons FilledButton.icon( onPressed: _startAttempt, icon: const Icon(Icons.play_arrow), label: const Text('Start Attempt'), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), ), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: _openConfig, icon: const Icon(Icons.settings), label: const Text('Configure Deck'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), ), ), ], ), ), ); } } class _StatItem extends StatelessWidget { final String label; final String value; final IconData icon; const _StatItem({ required this.label, required this.value, required this.icon, }); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 24), const SizedBox(height: 4), Text( value, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), Text( label, style: Theme.of(context).textTheme.bodySmall, ), ], ); } } class _HistoryStatItem extends StatelessWidget { final String label; final String value; final IconData icon; const _HistoryStatItem({ required this.label, required this.value, required this.icon, }); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary), const SizedBox(height: 4), Text( value, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), ), Text( label, style: Theme.of(context).textTheme.bodySmall, ), ], ); } }