flagging and exclude
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user