shufle order
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/top_snackbar.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
@@ -77,6 +78,8 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
||||
Set<int> _selectedAnswerIndices = {}; // For multiple answer questions
|
||||
Map<String, dynamic> _answers = {}; // Can store int or List<int>
|
||||
Map<String, bool> _manualOverrides = {};
|
||||
/// Display order of answer indices per question (shuffled for new attempts).
|
||||
Map<String, List<int>> _answerOrderPerQuestion = {};
|
||||
final DeckStorage _deckStorage = DeckStorage();
|
||||
late PageController _pageController;
|
||||
Timer? _timer;
|
||||
@@ -117,10 +120,13 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
||||
_currentQuestionIndex = incomplete.currentQuestionIndex;
|
||||
_answers = Map<String, dynamic>.from(incomplete.answers);
|
||||
_manualOverrides = Map<String, bool>.from(incomplete.manualOverrides);
|
||||
|
||||
// Resumed attempt: use identity order so saved answers (original indices) match
|
||||
_answerOrderPerQuestion = {
|
||||
for (final q in _attempt!.questions)
|
||||
q.id: List.generate(q.answers.length, (i) => i),
|
||||
};
|
||||
// Restore selected answer for current question
|
||||
_loadQuestionState();
|
||||
|
||||
// Initialize PageController to the current question index
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_pageController.hasClients) {
|
||||
@@ -132,6 +138,16 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
||||
deck: _deck!,
|
||||
includeKnown: includeKnown,
|
||||
);
|
||||
// New attempt: randomize answer order per question when deck config enables it
|
||||
_answerOrderPerQuestion = {};
|
||||
final shuffleAnswers = _deck!.config.shuffleAnswerOrder;
|
||||
for (final q in _attempt!.questions) {
|
||||
final order = List.generate(q.answers.length, (i) => i);
|
||||
if (shuffleAnswers) {
|
||||
order.shuffle(math.Random());
|
||||
}
|
||||
_answerOrderPerQuestion[q.id] = order;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize timer if time limit is set
|
||||
@@ -451,12 +467,11 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
||||
final updatedDeck = _deck!.copyWith(incompleteAttempt: incompleteAttempt);
|
||||
_deckStorage.saveDeckSync(updatedDeck);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Attempt saved. You can continue later.'),
|
||||
backgroundColor: Colors.blue,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Attempt saved. You can continue later.',
|
||||
backgroundColor: Colors.blue,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
Navigator.pop(context);
|
||||
@@ -709,58 +724,62 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
||||
QuestionCard(question: question),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Answer Options
|
||||
...List.generate(
|
||||
question.answers.length,
|
||||
(answerIndex) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: AnswerOption(
|
||||
text: question.answers[answerIndex],
|
||||
isSelected: isMultipleCorrect
|
||||
? selectedIndices.contains(answerIndex)
|
||||
: selectedIndex == answerIndex,
|
||||
onTap: () {
|
||||
// Update selection for this question
|
||||
setState(() {
|
||||
if (isMultipleCorrect) {
|
||||
if (selectedIndices.contains(answerIndex)) {
|
||||
selectedIndices.remove(answerIndex);
|
||||
} else {
|
||||
selectedIndices.add(answerIndex);
|
||||
}
|
||||
_answers[question.id] = selectedIndices.toList()..sort();
|
||||
} else {
|
||||
// Toggle: if already selected, deselect; otherwise select
|
||||
if (selectedIndex == answerIndex) {
|
||||
selectedIndex = null;
|
||||
_answers.remove(question.id);
|
||||
} else {
|
||||
selectedIndex = answerIndex;
|
||||
_answers[question.id] = selectedIndex;
|
||||
}
|
||||
}
|
||||
// Update current question state if viewing it
|
||||
if (index == _currentQuestionIndex) {
|
||||
if (isMultipleCorrect) {
|
||||
_selectedAnswerIndices = selectedIndices;
|
||||
_selectedAnswerIndex = null;
|
||||
} else {
|
||||
_selectedAnswerIndex = selectedIndex;
|
||||
_selectedAnswerIndices.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
isCorrect: _deck != null && _deck!.config.immediateFeedbackEnabled &&
|
||||
(isMultipleCorrect
|
||||
? selectedIndices.contains(answerIndex)
|
||||
: selectedIndex == answerIndex)
|
||||
? question.isCorrectAnswer(answerIndex)
|
||||
: null,
|
||||
isMultipleChoice: isMultipleCorrect,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Answer Options (displayed in shuffled order; we store original indices)
|
||||
...() {
|
||||
final order = _answerOrderPerQuestion[question.id] ??
|
||||
List.generate(question.answers.length, (i) => i);
|
||||
return List.generate(
|
||||
question.answers.length,
|
||||
(displayIndex) {
|
||||
final originalIndex = order[displayIndex];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: AnswerOption(
|
||||
text: question.answers[originalIndex],
|
||||
isSelected: isMultipleCorrect
|
||||
? selectedIndices.contains(originalIndex)
|
||||
: selectedIndex == originalIndex,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (isMultipleCorrect) {
|
||||
if (selectedIndices.contains(originalIndex)) {
|
||||
selectedIndices.remove(originalIndex);
|
||||
} else {
|
||||
selectedIndices.add(originalIndex);
|
||||
}
|
||||
_answers[question.id] = selectedIndices.toList()..sort();
|
||||
} else {
|
||||
if (selectedIndex == originalIndex) {
|
||||
selectedIndex = null;
|
||||
_answers.remove(question.id);
|
||||
} else {
|
||||
selectedIndex = originalIndex;
|
||||
_answers[question.id] = selectedIndex;
|
||||
}
|
||||
}
|
||||
if (index == _currentQuestionIndex) {
|
||||
if (isMultipleCorrect) {
|
||||
_selectedAnswerIndices = selectedIndices;
|
||||
_selectedAnswerIndex = null;
|
||||
} else {
|
||||
_selectedAnswerIndex = selectedIndex;
|
||||
_selectedAnswerIndices.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
isCorrect: _deck != null && _deck!.config.immediateFeedbackEnabled &&
|
||||
(isMultipleCorrect
|
||||
? selectedIndices.contains(originalIndex)
|
||||
: selectedIndex == originalIndex)
|
||||
? question.isCorrectAnswer(originalIndex)
|
||||
: null,
|
||||
isMultipleChoice: isMultipleCorrect,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -788,11 +807,10 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
||||
questionId: question.id,
|
||||
);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Question marked as Known'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Question marked as Known',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.check_circle),
|
||||
@@ -810,10 +828,9 @@ class _AttemptScreenState extends State<AttemptScreen> {
|
||||
questionId: question.id,
|
||||
);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Question marked as Needs Practice'),
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Question marked as Needs Practice',
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.school),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/top_snackbar.dart';
|
||||
import 'package:practice_engine/practice_engine.dart';
|
||||
import '../services/deck_storage.dart';
|
||||
import '../routes.dart';
|
||||
@@ -22,6 +23,7 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
|
||||
late TextEditingController _timeLimitSecondsController;
|
||||
late bool _immediateFeedback;
|
||||
late bool _includeKnownInAttempts;
|
||||
late bool _shuffleAnswerOrder;
|
||||
bool _timeLimitEnabled = false;
|
||||
String? _lastDeckId;
|
||||
int _configHashCode = 0;
|
||||
@@ -71,7 +73,8 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
|
||||
);
|
||||
_immediateFeedback = _config!.immediateFeedbackEnabled;
|
||||
_includeKnownInAttempts = _config!.includeKnownInAttempts;
|
||||
|
||||
_shuffleAnswerOrder = _config!.shuffleAnswerOrder;
|
||||
|
||||
// Initialize time limit controllers
|
||||
_timeLimitEnabled = _config!.timeLimitSeconds != null;
|
||||
final totalSeconds = _config!.timeLimitSeconds ?? 0;
|
||||
@@ -192,18 +195,18 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
|
||||
priorityDecreaseOnCorrect: priorityDecrease,
|
||||
immediateFeedbackEnabled: _immediateFeedback,
|
||||
includeKnownInAttempts: _includeKnownInAttempts,
|
||||
shuffleAnswerOrder: _shuffleAnswerOrder,
|
||||
timeLimitSeconds: timeLimitSeconds,
|
||||
);
|
||||
|
||||
final updatedDeck = _deck!.copyWith(config: updatedConfig);
|
||||
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Deck configuration saved successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Deck configuration saved successfully',
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
// Pop with the updated deck
|
||||
@@ -276,12 +279,11 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
|
||||
|
||||
// Show success message
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Deck has been reset successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Deck has been reset successfully',
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,6 +401,21 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Shuffle Answer Order Toggle
|
||||
SwitchListTile(
|
||||
title: const Text('Shuffle Answer Order'),
|
||||
subtitle: const Text(
|
||||
'Show answer options in random order each attempt',
|
||||
),
|
||||
value: _shuffleAnswerOrder,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_shuffleAnswerOrder = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Time Limit Section
|
||||
SwitchListTile(
|
||||
title: const Text('Enable Time Limit'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/top_snackbar.dart';
|
||||
import 'package:practice_engine/practice_engine.dart';
|
||||
import '../services/deck_storage.dart';
|
||||
import 'deck_edit_screen.dart';
|
||||
@@ -98,12 +99,11 @@ class _DeckCreateScreenState extends State<DeckCreateScreen> {
|
||||
|
||||
_deckStorage.saveDeckSync(newDeck);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Deck created successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Deck created successfully',
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
Navigator.pop(context, newDeck);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/top_snackbar.dart';
|
||||
import 'package:practice_engine/practice_engine.dart';
|
||||
import '../services/deck_storage.dart';
|
||||
|
||||
@@ -120,12 +121,11 @@ class _DeckEditScreenState extends State<DeckEditScreen> {
|
||||
|
||||
_deckStorage.saveDeckSync(updatedDeck);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Deck saved successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Deck saved successfully',
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
Navigator.pop(context, updatedDeck);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:practice_engine/practice_engine.dart';
|
||||
import '../data/default_deck.dart';
|
||||
import '../services/deck_storage.dart';
|
||||
import '../utils/top_snackbar.dart';
|
||||
|
||||
class DeckImportScreen extends StatefulWidget {
|
||||
const DeckImportScreen({super.key});
|
||||
@@ -35,6 +37,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
||||
priorityDecreaseOnCorrect: configJson['priorityDecreaseOnCorrect'] as int? ?? 2,
|
||||
immediateFeedbackEnabled: configJson['immediateFeedbackEnabled'] as bool? ?? true,
|
||||
includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false,
|
||||
shuffleAnswerOrder: configJson['shuffleAnswerOrder'] as bool? ?? true,
|
||||
);
|
||||
|
||||
// Parse questions
|
||||
@@ -173,11 +176,10 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
||||
} else if (action == 'replace') {
|
||||
deckStorage.saveDeckSync(defaultDeck);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Default deck replaced successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Default deck replaced successfully',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
} else if (action == 'copy') {
|
||||
// Find the next copy number
|
||||
@@ -210,22 +212,20 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
||||
);
|
||||
deckStorage.saveDeckSync(copiedDeck);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Default deck copied successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Default deck copied successfully',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Save default deck to storage
|
||||
deckStorage.saveDeckSync(defaultDeck);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Default deck added successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Default deck added successfully',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,6 +245,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
||||
'priorityDecreaseOnCorrect': 2,
|
||||
'immediateFeedbackEnabled': true,
|
||||
'includeKnownInAttempts': false,
|
||||
'shuffleAnswerOrder': true,
|
||||
},
|
||||
'questions': List.generate(20, (i) {
|
||||
return {
|
||||
@@ -260,6 +261,16 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
||||
_jsonController.text = const JsonEncoder.withIndent(' ').convert(sampleJson);
|
||||
}
|
||||
|
||||
void _copyFieldToClipboard() {
|
||||
final text = _jsonController.text.trim();
|
||||
if (text.isEmpty) {
|
||||
showTopSnackBar(context, message: 'Nothing to copy');
|
||||
return;
|
||||
}
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
showTopSnackBar(context, message: 'JSON copied to clipboard');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -379,10 +390,20 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
||||
'Deck JSON',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: _loadSampleDeck,
|
||||
icon: const Icon(Icons.description),
|
||||
label: const Text('Load Sample'),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: _loadSampleDeck,
|
||||
icon: const Icon(Icons.description),
|
||||
label: const Text('Load Sample'),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _copyFieldToClipboard,
|
||||
icon: const Icon(Icons.copy),
|
||||
tooltip: 'Copy field contents',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -491,6 +512,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
|
||||
const Text('• priorityDecreaseOnCorrect: number'),
|
||||
const Text('• immediateFeedbackEnabled: boolean'),
|
||||
const Text('• includeKnownInAttempts: boolean'),
|
||||
const Text('• shuffleAnswerOrder: boolean'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ 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});
|
||||
@@ -73,11 +74,10 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
||||
_deckStorage.deleteDeckSync(deck.id);
|
||||
_loadDecks();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${deck.title} deleted'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: '${deck.title} deleted',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -125,11 +125,10 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
||||
_loadDecks();
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${deck.title} cloned successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: '${deck.title} cloned successfully',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -242,11 +241,10 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
||||
_deckStorage.saveDeckSync(defaultDeck);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Default deck replaced successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Default deck replaced successfully',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
} else if (action == 'copy') {
|
||||
@@ -281,11 +279,10 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
||||
_deckStorage.saveDeckSync(copiedDeck);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Default deck copied successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Default deck copied successfully',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -294,11 +291,10 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
||||
_deckStorage.saveDeckSync(defaultDeck);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Default deck added successfully'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: 'Default deck added successfully',
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -495,7 +491,7 @@ class _DeckListScreenState extends State<DeckListScreen> {
|
||||
Expanded(
|
||||
child: _StatChip(
|
||||
icon: Icons.trending_up,
|
||||
label: '${deck.practicePercentage.toStringAsFixed(0)}%',
|
||||
label: '${deck.progressPercentage.toStringAsFixed(0)}%',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:practice_engine/practice_engine.dart';
|
||||
import '../routes.dart';
|
||||
import '../services/deck_storage.dart';
|
||||
import '../utils/top_snackbar.dart';
|
||||
|
||||
class DeckOverviewScreen extends StatefulWidget {
|
||||
const DeckOverviewScreen({super.key});
|
||||
@@ -252,14 +253,13 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
|
||||
|
||||
// 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),
|
||||
),
|
||||
showTopSnackBar(
|
||||
context,
|
||||
message: wasReset
|
||||
? 'Deck has been reset successfully'
|
||||
: 'Deck settings have been updated',
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -467,7 +467,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: CircularProgressIndicator(
|
||||
value: _deck!.practicePercentage / 100,
|
||||
value: _deck!.progressPercentage / 100,
|
||||
strokeWidth: 6,
|
||||
),
|
||||
),
|
||||
@@ -477,7 +477,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${_deck!.practicePercentage.toStringAsFixed(1)}%',
|
||||
'${_deck!.progressPercentage.toStringAsFixed(1)}%',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
||||
@@ -59,6 +59,7 @@ class DeckStorage {
|
||||
'priorityDecreaseOnCorrect': deck.config.priorityDecreaseOnCorrect,
|
||||
'immediateFeedbackEnabled': deck.config.immediateFeedbackEnabled,
|
||||
'includeKnownInAttempts': deck.config.includeKnownInAttempts,
|
||||
'shuffleAnswerOrder': deck.config.shuffleAnswerOrder,
|
||||
'timeLimitSeconds': deck.config.timeLimitSeconds,
|
||||
},
|
||||
'questions': deck.questions.map((q) => {
|
||||
@@ -105,6 +106,7 @@ class DeckStorage {
|
||||
priorityDecreaseOnCorrect: configJson['priorityDecreaseOnCorrect'] as int? ?? 2,
|
||||
immediateFeedbackEnabled: configJson['immediateFeedbackEnabled'] as bool? ?? true,
|
||||
includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false,
|
||||
shuffleAnswerOrder: configJson['shuffleAnswerOrder'] as bool? ?? true,
|
||||
timeLimitSeconds: configJson['timeLimitSeconds'] as int?,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Shows a snackbar-style message at the top of the screen (for success/neutral feedback).
|
||||
void showTopSnackBar(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
Color? backgroundColor,
|
||||
Duration duration = const Duration(seconds: 2),
|
||||
}) {
|
||||
final overlay = Overlay.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final color = backgroundColor ?? theme.colorScheme.surfaceContainerHighest;
|
||||
final textColor = backgroundColor != null ? Colors.white : theme.colorScheme.onSurface;
|
||||
|
||||
late OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Material(
|
||||
color: color,
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Text(
|
||||
message,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: textColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(entry);
|
||||
Future.delayed(duration, () {
|
||||
entry.remove();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user