flagging and exclude

master 1.0
gitea 4 weeks ago
parent 10a4837ee0
commit fe9c5a9fec

@ -24,6 +24,7 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
late bool _immediateFeedback; late bool _immediateFeedback;
late bool _includeKnownInAttempts; late bool _includeKnownInAttempts;
late bool _shuffleAnswerOrder; late bool _shuffleAnswerOrder;
late bool _excludeFlaggedQuestions;
bool _timeLimitEnabled = false; bool _timeLimitEnabled = false;
String? _lastDeckId; String? _lastDeckId;
int _configHashCode = 0; int _configHashCode = 0;
@ -74,6 +75,7 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
_immediateFeedback = _config!.immediateFeedbackEnabled; _immediateFeedback = _config!.immediateFeedbackEnabled;
_includeKnownInAttempts = _config!.includeKnownInAttempts; _includeKnownInAttempts = _config!.includeKnownInAttempts;
_shuffleAnswerOrder = _config!.shuffleAnswerOrder; _shuffleAnswerOrder = _config!.shuffleAnswerOrder;
_excludeFlaggedQuestions = _config!.excludeFlaggedQuestions;
// Initialize time limit controllers // Initialize time limit controllers
_timeLimitEnabled = _config!.timeLimitSeconds != null; _timeLimitEnabled = _config!.timeLimitSeconds != null;
@ -196,6 +198,7 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
immediateFeedbackEnabled: _immediateFeedback, immediateFeedbackEnabled: _immediateFeedback,
includeKnownInAttempts: _includeKnownInAttempts, includeKnownInAttempts: _includeKnownInAttempts,
shuffleAnswerOrder: _shuffleAnswerOrder, shuffleAnswerOrder: _shuffleAnswerOrder,
excludeFlaggedQuestions: _excludeFlaggedQuestions,
timeLimitSeconds: timeLimitSeconds, timeLimitSeconds: timeLimitSeconds,
); );
@ -416,6 +419,21 @@ class _DeckConfigScreenState extends State<DeckConfigScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Exclude Flagged Questions Toggle
SwitchListTile(
title: const Text('Exclude flagged questions'),
subtitle: const Text(
'Flagged questions are not used in attempts and do not count in progress',
),
value: _excludeFlaggedQuestions,
onChanged: (value) {
setState(() {
_excludeFlaggedQuestions = value;
});
},
),
const SizedBox(height: 16),
// Time Limit Section // Time Limit Section
SwitchListTile( SwitchListTile(
title: const Text('Enable Time Limit'), title: const Text('Enable Time Limit'),

@ -40,6 +40,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
immediateFeedbackEnabled: configJson['immediateFeedbackEnabled'] as bool? ?? true, immediateFeedbackEnabled: configJson['immediateFeedbackEnabled'] as bool? ?? true,
includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false, includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false,
shuffleAnswerOrder: configJson['shuffleAnswerOrder'] as bool? ?? true, shuffleAnswerOrder: configJson['shuffleAnswerOrder'] as bool? ?? true,
excludeFlaggedQuestions: configJson['excludeFlaggedQuestions'] as bool? ?? false,
); );
// Parse questions // Parse questions
@ -249,6 +250,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
'immediateFeedbackEnabled': true, 'immediateFeedbackEnabled': true,
'includeKnownInAttempts': false, 'includeKnownInAttempts': false,
'shuffleAnswerOrder': true, 'shuffleAnswerOrder': true,
'excludeFlaggedQuestions': false,
}, },
'questions': List.generate(20, (i) { 'questions': List.generate(20, (i) {
return { return {
@ -545,6 +547,7 @@ class _DeckImportScreenState extends State<DeckImportScreen> {
const Text('• immediateFeedbackEnabled: boolean'), const Text('• immediateFeedbackEnabled: boolean'),
const Text('• includeKnownInAttempts: boolean'), const Text('• includeKnownInAttempts: boolean'),
const Text('• shuffleAnswerOrder: boolean'), const Text('• shuffleAnswerOrder: boolean'),
const Text('• excludeFlaggedQuestions: boolean'),
], ],
), ),
), ),

@ -59,10 +59,14 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
} }
} }
if (deckToCheck == null || deckToCheck.questions.isEmpty) { if (deckToCheck == null || deckToCheck.activeQuestionCount == 0) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Cannot start attempt: No questions in deck'), content: Text(
deckToCheck != null && deckToCheck.questions.isNotEmpty && deckToCheck.config.excludeFlaggedQuestions
? 'Cannot start attempt: All questions are flagged. Unflag some or turn off "Exclude flagged questions" in settings.'
: 'Cannot start attempt: No questions in deck',
),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@ -158,8 +162,8 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
} }
} }
// Check if all questions are known and includeKnownInAttempts is disabled // Check if all active questions are known and includeKnownInAttempts is disabled
final allKnown = deckToCheck.knownCount == deckToCheck.numberOfQuestions && deckToCheck.numberOfQuestions > 0; final allKnown = deckToCheck.knownCount == deckToCheck.activeQuestionCount && deckToCheck.activeQuestionCount > 0;
final includeKnownDisabled = !deckToCheck.config.includeKnownInAttempts; final includeKnownDisabled = !deckToCheck.config.includeKnownInAttempts;
if (allKnown && includeKnownDisabled) { if (allKnown && includeKnownDisabled) {
@ -285,7 +289,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
Widget _buildProgressStats(BuildContext context) { Widget _buildProgressStats(BuildContext context) {
if (_deck == null) return const SizedBox.shrink(); if (_deck == null) return const SizedBox.shrink();
final totalQuestions = _deck!.numberOfQuestions; final totalQuestions = _deck!.activeQuestionCount;
final knownCount = _deck!.knownCount; final knownCount = _deck!.knownCount;
final unknownCount = totalQuestions - knownCount; final unknownCount = totalQuestions - knownCount;
final unknownPercentage = totalQuestions > 0 final unknownPercentage = totalQuestions > 0
@ -484,7 +488,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${_deck!.knownCount} of ${_deck!.numberOfQuestions} questions known', '${_deck!.knownCount} of ${_deck!.activeQuestionCount} questions known',
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
], ],
@ -505,7 +509,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
children: [ children: [
_StatItem( _StatItem(
label: 'Total', label: 'Total',
value: '${_deck!.numberOfQuestions}', value: '${_deck!.activeQuestionCount}',
icon: Icons.quiz, icon: Icons.quiz,
), ),
_StatItem( _StatItem(
@ -515,7 +519,7 @@ class _DeckOverviewScreenState extends State<DeckOverviewScreen> {
), ),
_StatItem( _StatItem(
label: 'Practice', label: 'Practice',
value: '${_deck!.numberOfQuestions - _deck!.knownCount}', value: '${_deck!.activeQuestionCount - _deck!.knownCount}',
icon: Icons.school, icon: Icons.school,
), ),
], ],

@ -60,6 +60,7 @@ class DeckStorage {
'immediateFeedbackEnabled': deck.config.immediateFeedbackEnabled, 'immediateFeedbackEnabled': deck.config.immediateFeedbackEnabled,
'includeKnownInAttempts': deck.config.includeKnownInAttempts, 'includeKnownInAttempts': deck.config.includeKnownInAttempts,
'shuffleAnswerOrder': deck.config.shuffleAnswerOrder, 'shuffleAnswerOrder': deck.config.shuffleAnswerOrder,
'excludeFlaggedQuestions': deck.config.excludeFlaggedQuestions,
'timeLimitSeconds': deck.config.timeLimitSeconds, 'timeLimitSeconds': deck.config.timeLimitSeconds,
}, },
'questions': deck.questions.map((q) => { 'questions': deck.questions.map((q) => {
@ -108,6 +109,7 @@ class DeckStorage {
immediateFeedbackEnabled: configJson['immediateFeedbackEnabled'] as bool? ?? true, immediateFeedbackEnabled: configJson['immediateFeedbackEnabled'] as bool? ?? true,
includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false, includeKnownInAttempts: configJson['includeKnownInAttempts'] as bool? ?? false,
shuffleAnswerOrder: configJson['shuffleAnswerOrder'] as bool? ?? true, shuffleAnswerOrder: configJson['shuffleAnswerOrder'] as bool? ?? true,
excludeFlaggedQuestions: configJson['excludeFlaggedQuestions'] as bool? ?? false,
timeLimitSeconds: configJson['timeLimitSeconds'] as int?, timeLimitSeconds: configJson['timeLimitSeconds'] as int?,
); );

@ -27,11 +27,14 @@ class AttemptService {
final size = attemptSize ?? deck.config.defaultAttemptSize; final size = attemptSize ?? deck.config.defaultAttemptSize;
// Filter candidates based on includeKnownInAttempts setting // Filter candidates based on includeKnownInAttempts setting
// Use override parameter if provided, otherwise use config
final shouldIncludeKnown = includeKnown ?? deck.config.includeKnownInAttempts; final shouldIncludeKnown = includeKnown ?? deck.config.includeKnownInAttempts;
final candidates = shouldIncludeKnown var candidates = shouldIncludeKnown
? deck.questions ? List<Question>.from(deck.questions)
: deck.questions.where((q) => !q.isKnown).toList(); : deck.questions.where((q) => !q.isKnown).toList();
// Exclude flagged questions when config says so
if (deck.config.excludeFlaggedQuestions) {
candidates = candidates.where((q) => !q.isFlagged).toList();
}
final selected = _selector.selectQuestions( final selected = _selector.selectQuestions(
candidates: candidates, candidates: candidates,

@ -67,8 +67,17 @@ class Deck {
/// Total number of questions in the deck. /// Total number of questions in the deck.
int get numberOfQuestions => questions.length; int get numberOfQuestions => questions.length;
/// Number of questions marked as known. /// Questions that count for attempts and statistics (excludes flagged when config says so).
int get knownCount => questions.where((q) => q.isKnown).length; List<Question> get _activeQuestions =>
config.excludeFlaggedQuestions
? questions.where((q) => !q.isFlagged).toList()
: questions;
/// Number of active questions (excludes flagged when [DeckConfig.excludeFlaggedQuestions] is true).
int get activeQuestionCount => _activeQuestions.length;
/// Number of questions marked as known (among active questions only).
int get knownCount => _activeQuestions.where((q) => q.isKnown).length;
/// Number of questions the user has flagged. /// Number of questions the user has flagged.
int get flaggedCount => questions.where((q) => q.isFlagged).length; int get flaggedCount => questions.where((q) => q.isFlagged).length;
@ -76,24 +85,24 @@ class Deck {
/// Questions that the user has flagged. /// Questions that the user has flagged.
List<Question> get flaggedQuestions => questions.where((q) => q.isFlagged).toList(); List<Question> get flaggedQuestions => questions.where((q) => q.isFlagged).toList();
/// Practice percentage: (known / total) * 100 /// Practice percentage: (known among active / active count) * 100
double get practicePercentage { double get practicePercentage {
if (questions.isEmpty) return 0.0; final active = _activeQuestions;
return (knownCount / questions.length) * 100.0; if (active.isEmpty) return 0.0;
return (knownCount / active.length) * 100.0;
} }
/// Progress percentage including partial credit: each question contributes /// Progress percentage including partial credit over active questions only.
/// 1.0 if known, otherwise (consecutiveCorrect / requiredConsecutiveCorrect)
/// capped at 1.0. Averaged over all questions * 100.
double get progressPercentage { double get progressPercentage {
if (questions.isEmpty) return 0.0; final active = _activeQuestions;
if (active.isEmpty) return 0.0;
final required = config.requiredConsecutiveCorrect; final required = config.requiredConsecutiveCorrect;
final sum = questions.fold<double>(0.0, (sum, q) { final sum = active.fold<double>(0.0, (sum, q) {
if (q.isKnown) return sum + 1.0; if (q.isKnown) return sum + 1.0;
final partial = (q.consecutiveCorrect / required).clamp(0.0, 1.0); final partial = (q.consecutiveCorrect / required).clamp(0.0, 1.0);
return sum + partial; return sum + partial;
}); });
return (sum / questions.length) * 100.0; return (sum / active.length) * 100.0;
} }
/// Number of completed attempts. /// Number of completed attempts.

@ -23,6 +23,10 @@ class DeckConfig {
/// When true, answer options appear in random order each attempt. /// When true, answer options appear in random order each attempt.
final bool shuffleAnswerOrder; final bool shuffleAnswerOrder;
/// Whether to exclude flagged questions from attempts and statistics.
/// When true, flagged questions are not selected for attempts and do not count in progress/known.
final bool excludeFlaggedQuestions;
/// Optional time limit for attempts in seconds. /// Optional time limit for attempts in seconds.
/// If null, no time limit is enforced. /// If null, no time limit is enforced.
final int? timeLimitSeconds; final int? timeLimitSeconds;
@ -35,6 +39,7 @@ class DeckConfig {
this.immediateFeedbackEnabled = true, this.immediateFeedbackEnabled = true,
this.includeKnownInAttempts = false, this.includeKnownInAttempts = false,
this.shuffleAnswerOrder = true, this.shuffleAnswerOrder = true,
this.excludeFlaggedQuestions = false,
this.timeLimitSeconds, this.timeLimitSeconds,
}); });
@ -47,6 +52,7 @@ class DeckConfig {
bool? immediateFeedbackEnabled, bool? immediateFeedbackEnabled,
bool? includeKnownInAttempts, bool? includeKnownInAttempts,
bool? shuffleAnswerOrder, bool? shuffleAnswerOrder,
bool? excludeFlaggedQuestions,
int? timeLimitSeconds, int? timeLimitSeconds,
}) { }) {
return DeckConfig( return DeckConfig(
@ -62,6 +68,8 @@ class DeckConfig {
includeKnownInAttempts: includeKnownInAttempts:
includeKnownInAttempts ?? this.includeKnownInAttempts, includeKnownInAttempts ?? this.includeKnownInAttempts,
shuffleAnswerOrder: shuffleAnswerOrder ?? this.shuffleAnswerOrder, shuffleAnswerOrder: shuffleAnswerOrder ?? this.shuffleAnswerOrder,
excludeFlaggedQuestions:
excludeFlaggedQuestions ?? this.excludeFlaggedQuestions,
timeLimitSeconds: timeLimitSeconds ?? this.timeLimitSeconds, timeLimitSeconds: timeLimitSeconds ?? this.timeLimitSeconds,
); );
} }
@ -78,6 +86,7 @@ class DeckConfig {
immediateFeedbackEnabled == other.immediateFeedbackEnabled && immediateFeedbackEnabled == other.immediateFeedbackEnabled &&
includeKnownInAttempts == other.includeKnownInAttempts && includeKnownInAttempts == other.includeKnownInAttempts &&
shuffleAnswerOrder == other.shuffleAnswerOrder && shuffleAnswerOrder == other.shuffleAnswerOrder &&
excludeFlaggedQuestions == other.excludeFlaggedQuestions &&
timeLimitSeconds == other.timeLimitSeconds; timeLimitSeconds == other.timeLimitSeconds;
@override @override
@ -89,6 +98,7 @@ class DeckConfig {
immediateFeedbackEnabled.hashCode ^ immediateFeedbackEnabled.hashCode ^
includeKnownInAttempts.hashCode ^ includeKnownInAttempts.hashCode ^
shuffleAnswerOrder.hashCode ^ shuffleAnswerOrder.hashCode ^
excludeFlaggedQuestions.hashCode ^
(timeLimitSeconds?.hashCode ?? 0); (timeLimitSeconds?.hashCode ?? 0);
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.