import 'package:flutter/material.dart'; import 'package:practice_engine/practice_engine.dart'; import '../routes.dart'; import '../services/deck_storage.dart'; import '../data/default_deck.dart'; import '../utils/top_snackbar.dart'; class DeckListScreen extends StatefulWidget { const DeckListScreen({super.key}); @override State createState() => _DeckListScreenState(); } class _DeckListScreenState extends State { final DeckStorage _deckStorage = DeckStorage(); List _decks = []; @override void initState() { super.initState(); _loadDecks(); } void _loadDecks() { // Use sync version for immediate UI update, will work once storage is initialized setState(() { _decks = _deckStorage.getAllDecksSync(); }); // Also trigger async load to ensure we have latest data _deckStorage.getAllDecks().then((decks) { if (mounted) { setState(() { _decks = decks; }); } }); } void _openDeck(Deck deck) { Navigator.pushNamed( context, Routes.deckOverview, arguments: deck, ).then((_) { // Reload decks when returning from overview (in case deck was updated) _loadDecks(); }); } void _deleteDeck(Deck deck) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete Deck'), content: Text('Are you sure you want to delete "${deck.title}"? This action cannot be undone.'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(context, true), style: FilledButton.styleFrom( backgroundColor: Colors.red, ), child: const Text('Delete'), ), ], ), ); if (confirmed == true) { _deckStorage.deleteDeckSync(deck.id); _loadDecks(); if (mounted) { showTopSnackBar( context, message: '${deck.title} deleted', backgroundColor: Colors.green, ); } } } void _navigateToImport() { Navigator.pushNamed(context, Routes.deckImport).then((_) { // Reload decks when returning from import _loadDecks(); }); } void _editDeck(Deck deck) { Navigator.pushNamed( context, Routes.deckEdit, arguments: deck, ).then((updatedDeck) { if (updatedDeck != null) { _loadDecks(); } }); } void _cloneDeck(Deck deck) { // Create a copy of the deck with a new ID and reset progress final clonedDeck = deck.copyWith( id: '${deck.id}_clone_${DateTime.now().millisecondsSinceEpoch}', title: '${deck.title} (Copy)', currentAttemptIndex: 0, attemptHistory: [], questions: deck.questions.map((q) { return q.copyWith( consecutiveCorrect: 0, isKnown: false, priorityPoints: 0, lastAttemptIndex: -1, totalCorrectAttempts: 0, totalAttempts: 0, ); }).toList(), ); _deckStorage.saveDeckSync(clonedDeck); _loadDecks(); if (mounted) { showTopSnackBar( context, message: '${deck.title} cloned successfully', backgroundColor: Colors.green, ); } } void _showAddDeckOptions() { showModalBottomSheet( context: context, builder: (context) => SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Add New Deck', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), // Import JSON ListTile( leading: const Icon(Icons.upload_file), title: const Text('Import JSON'), subtitle: const Text('Import a deck from JSON format'), onTap: () { Navigator.pop(context); _navigateToImport(); }, ), const Divider(), // Create Manually ListTile( leading: const Icon(Icons.edit), title: const Text('Create Manually'), subtitle: const Text('Create a new deck from scratch'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, Routes.deckCreate).then((_) { _loadDecks(); }); }, ), const Divider(), // Use Default Quiz ListTile( leading: const Icon(Icons.quiz), title: const Text('Use Default Quiz'), subtitle: const Text('Add the default general knowledge quiz'), onTap: () { Navigator.pop(context); _useDefaultDeck(); }, ), const SizedBox(height: 8), ], ), ), ), ); } void _useDefaultDeck() async { // Always show dialog to ask if user wants mock data final includeMockData = await showDialog( context: context, builder: (context) => _MockDataDialog(), ); if (includeMockData == null) { // User cancelled return; } // Get the appropriate deck version final defaultDeck = includeMockData ? DefaultDeck.deckWithMockData : DefaultDeck.deck; final baseDeck = DefaultDeck.deck; final deckExists = _deckStorage.hasDeckSync(baseDeck.id); if (deckExists) { // Show dialog with options: Replace, Add Copy, or Cancel final action = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Deck Already Exists'), content: const Text( 'The General Knowledge Quiz already exists. What would you like to do?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, 'cancel'), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(context, 'copy'), child: const Text('Add Copy'), ), FilledButton( onPressed: () => Navigator.pop(context, 'replace'), child: const Text('Replace'), ), ], ), ); if (action == null || action == 'cancel') { return; } else if (action == 'replace') { _deckStorage.saveDeckSync(defaultDeck); if (mounted) { showTopSnackBar( context, message: 'Default deck replaced successfully', backgroundColor: Colors.green, ); } } else if (action == 'copy') { // Find the next copy number final allDecks = _deckStorage.getAllDecksSync(); final baseTitle = defaultDeck.title; final copyPattern = RegExp(r'^(.+?)\s*\(Copy\s+(\d+)\)$'); int maxCopyNumber = 0; for (final deck in allDecks) { if (deck.title == baseTitle) { // Original deck exists continue; } final match = copyPattern.firstMatch(deck.title); if (match != null) { final titlePart = match.group(1)?.trim(); if (titlePart == baseTitle) { final copyNum = int.tryParse(match.group(2) ?? '0') ?? 0; if (copyNum > maxCopyNumber) { maxCopyNumber = copyNum; } } } } final nextCopyNumber = maxCopyNumber + 1; final copiedDeck = defaultDeck.copyWith( id: '${defaultDeck.id}_${DateTime.now().millisecondsSinceEpoch}', title: '$baseTitle (Copy $nextCopyNumber)', ); _deckStorage.saveDeckSync(copiedDeck); if (mounted) { showTopSnackBar( context, message: 'Default deck copied successfully', backgroundColor: Colors.green, ); } } } else { // Save default deck to storage _deckStorage.saveDeckSync(defaultDeck); if (mounted) { showTopSnackBar( context, message: 'Default deck added successfully', backgroundColor: Colors.green, ); } } _loadDecks(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('omotomo'), actions: [ IconButton( icon: const Icon(Icons.add), tooltip: 'Add Deck', onPressed: _showAddDeckOptions, ), ], ), body: _decks.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.inbox, size: 64, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), ), const SizedBox(height: 16), Text( 'No decks yet', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'Tap the + button to add a deck', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ), ) : RefreshIndicator( onRefresh: () async { _loadDecks(); }, child: ListView.builder( padding: const EdgeInsets.all(12), itemCount: _decks.length, itemBuilder: (context, index) { final deck = _decks[index]; final isCompleted = deck.knownCount == deck.numberOfQuestions && deck.numberOfQuestions > 0; return Card( margin: const EdgeInsets.only(bottom: 12), child: InkWell( onTap: () => _openDeck(deck), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( deck.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), if (isCompleted) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.green.shade700, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle, size: 16, color: Colors.white, ), const SizedBox(width: 4), Text( 'Completed', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), ), ], ), ), ], ], ), if (deck.description.isNotEmpty) ...[ const SizedBox(height: 4), Text( deck.description, style: Theme.of(context).textTheme.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ], ), ), PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( child: const Row( children: [ Icon(Icons.edit, color: Colors.blue), SizedBox(width: 8), Text('Edit'), ], ), onTap: () { Future.delayed( const Duration(milliseconds: 100), () => _editDeck(deck), ); }, ), PopupMenuItem( child: const Row( children: [ Icon(Icons.copy, color: Colors.orange), SizedBox(width: 8), Text('Clone'), ], ), onTap: () { Future.delayed( const Duration(milliseconds: 100), () => _cloneDeck(deck), ); }, ), PopupMenuItem( child: const Row( children: [ Icon(Icons.delete, color: Colors.red), SizedBox(width: 8), Text('Delete'), ], ), onTap: () { Future.delayed( const Duration(milliseconds: 100), () => _deleteDeck(deck), ); }, ), ], ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _StatChip( icon: Icons.quiz, label: '${deck.numberOfQuestions} questions', ), ), const SizedBox(width: 8), Expanded( child: _StatChip( icon: Icons.check_circle, label: '${deck.knownCount} known', ), ), const SizedBox(width: 8), Expanded( child: _StatChip( icon: Icons.trending_up, label: '${deck.progressPercentage.toStringAsFixed(0)}%', ), ), ], ), ], ), ), ), ); }, ), ), ); } } class _StatChip extends StatelessWidget { final IconData icon; final String label; const _StatChip({ required this.icon, required this.label, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16), const SizedBox(width: 4), Flexible( child: Text( label, style: Theme.of(context).textTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ), ], ), ); } } class _MockDataDialog extends StatefulWidget { @override State<_MockDataDialog> createState() => _MockDataDialogState(); } class _MockDataDialogState extends State<_MockDataDialog> { bool _includeMockData = false; @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Add Default Deck'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Would you like to include mock attempt history and progress data?', style: TextStyle(fontSize: 14), ), const SizedBox(height: 16), CheckboxListTile( value: _includeMockData, onChanged: (value) { setState(() { _includeMockData = value ?? false; }); }, title: const Text('Include mock data'), subtitle: const Text( 'Adds 4 sample attempts showing progress over time', style: TextStyle(fontSize: 12), ), contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context, null), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(context, _includeMockData), child: const Text('Add Deck'), ), ], ); } }