fix(app): study options saved as they came stay the defaults
A review found three edges of the study options: - Saving a studyset's study options without changing anything wrote the defaults as its own, so it stopped following the person's new cards a day and was written in the newer schema older apps skip. It now keeps no options of its own. - The library's badges read the person's new cards a day only when something else changed; a change to the setting now refreshes them. - A person's random order shuffled the whole round, undoing the studyset's Reviews first or New first. The reviews and the new cards are now each shuffled, the order between them kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1921043925
commit
6ba229be2c
6 changed files with 60 additions and 4 deletions
|
|
@ -122,7 +122,7 @@ class StudySessionBloc(
|
||||||
for (final group in groups) [for (final card in group) card.id],
|
for (final group in groups) [for (final card in group) card.id],
|
||||||
];
|
];
|
||||||
final queue = _queue(schedules, now);
|
final queue = _queue(schedules, now);
|
||||||
_round = _ordered(queue).take(_options.cardsPerRound).toList();
|
_round = queue.take(_options.cardsPerRound).toList();
|
||||||
if (_round.isEmpty) {
|
if (_round.isEmpty) {
|
||||||
final upcoming = [
|
final upcoming = [
|
||||||
for (final schedule in schedules.values) ?schedule.due,
|
for (final schedule in schedules.values) ?schedule.due,
|
||||||
|
|
@ -140,6 +140,9 @@ class StudySessionBloc(
|
||||||
groups: _groupIds,
|
groups: _groupIds,
|
||||||
options: _groupOptions,
|
options: _groupOptions,
|
||||||
order: _order,
|
order: _order,
|
||||||
|
// Shuffled within reviews and new cards, so that the studyset's
|
||||||
|
// order between them holds.
|
||||||
|
shuffle: _options.effectiveOrder == StudyOrder.random ? _random : null,
|
||||||
schedules: schedules,
|
schedules: schedules,
|
||||||
now: now,
|
now: now,
|
||||||
startOfToday: startOfLocalDay(now),
|
startOfToday: startOfLocalDay(now),
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/studyset_study_opt
|
||||||
/// most each studyset's new cards a day. A day is the local one that began at
|
/// most each studyset's new cards a day. A day is the local one that began at
|
||||||
/// [startOfToday], and what was reviewed or started in it counts. [order]
|
/// [startOfToday], and what was reviewed or started in it counts. [order]
|
||||||
/// (the studied studyset's) says whether the due cards or the new ones come
|
/// (the studied studyset's) says whether the due cards or the new ones come
|
||||||
/// first, or the new ones spread among the due. When nothing else is due, a
|
/// first, or the new ones spread among the due; with [shuffle], the due
|
||||||
/// learning card due within [learnAhead] comes early.
|
/// cards and the new ones are each shuffled first, keeping that order between
|
||||||
|
/// them. When nothing else is due, a learning card due within [learnAhead]
|
||||||
|
/// comes early.
|
||||||
List<String> studyQueue({
|
List<String> studyQueue({
|
||||||
required List<List<String>> groups,
|
required List<List<String>> groups,
|
||||||
required Map<String, CardSchedule> schedules,
|
required Map<String, CardSchedule> schedules,
|
||||||
|
|
@ -26,6 +28,7 @@ List<String> studyQueue({
|
||||||
List<StudysetStudyOptions> options = const [],
|
List<StudysetStudyOptions> options = const [],
|
||||||
int newPerDay = 20,
|
int newPerDay = 20,
|
||||||
NewCardOrder order = NewCardOrder.reviewsFirst,
|
NewCardOrder order = NewCardOrder.reviewsFirst,
|
||||||
|
math.Random? shuffle,
|
||||||
Duration learnAhead = const Duration(minutes: 20),
|
Duration learnAhead = const Duration(minutes: 20),
|
||||||
}) {
|
}) {
|
||||||
DateTime? dueOf(String id) => schedules[id]?.due;
|
DateTime? dueOf(String id) => schedules[id]?.due;
|
||||||
|
|
@ -68,6 +71,10 @@ List<String> studyQueue({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
due.sort(byDue);
|
due.sort(byDue);
|
||||||
|
if (shuffle != null) {
|
||||||
|
due.shuffle(shuffle);
|
||||||
|
fresh.shuffle(shuffle);
|
||||||
|
}
|
||||||
final queue = switch (order) {
|
final queue = switch (order) {
|
||||||
NewCardOrder.reviewsFirst => [...due, ...fresh],
|
NewCardOrder.reviewsFirst => [...due, ...fresh],
|
||||||
NewCardOrder.newFirst => [...fresh, ...due],
|
NewCardOrder.newFirst => [...fresh, ...due],
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,13 @@ class _StudyOptionsSheetState() extends State<_StudyOptionsSheet> {
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.of(context).pop((options: _options)),
|
// Saved as they came, the defaults stay the defaults: the
|
||||||
|
// studyset keeps following the person's study settings.
|
||||||
|
onPressed: () => Navigator.of(context).pop((
|
||||||
|
options: widget.current == null && _options == widget.defaults
|
||||||
|
? null
|
||||||
|
: _options,
|
||||||
|
)),
|
||||||
child: Text(l10n.commonSave),
|
child: Text(l10n.commonSave),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,13 @@ class StudysetListBloc(
|
||||||
_paths.changes,
|
_paths.changes,
|
||||||
onData: (_) => add(const StudysetListEventProgressRefreshRequested()),
|
onData: (_) => add(const StudysetListEventProgressRefreshRequested()),
|
||||||
),
|
),
|
||||||
|
// So do the person's new cards a day, which studysets without study
|
||||||
|
// options of their own follow.
|
||||||
|
emit.onEach(
|
||||||
|
_settings.watch(),
|
||||||
|
onData: (_) => add(const StudysetListEventProgressRefreshRequested()),
|
||||||
|
onError: (_, _) {},
|
||||||
|
),
|
||||||
emit.onEach(
|
emit.onEach(
|
||||||
_deletions.watchPending(),
|
_deletions.watchPending(),
|
||||||
onData: (pending) {
|
onData: (pending) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/model/card_schedule.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/model/card_schedule.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/model/study_queue.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/model/study_queue.dart';
|
||||||
|
|
@ -196,6 +198,24 @@ void main() {
|
||||||
expect(queue, ['n1', 'n2', 'due']);
|
expect(queue, ['n1', 'n2', 'due']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shuffled, the reviews still come before the new cards', () {
|
||||||
|
final queue = studyQueue(
|
||||||
|
groups: [
|
||||||
|
['d1', 'd2', 'd3', 'n1', 'n2', 'n3'],
|
||||||
|
],
|
||||||
|
shuffle: math.Random(7),
|
||||||
|
schedules: {
|
||||||
|
for (final (i, id) in ['d1', 'd2', 'd3'].indexed)
|
||||||
|
id: review(overdue.add(Duration(minutes: i))),
|
||||||
|
},
|
||||||
|
now: now,
|
||||||
|
startOfToday: startOfToday,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queue.take(3).toSet(), {'d1', 'd2', 'd3'});
|
||||||
|
expect(queue.skip(3).toSet(), {'n1', 'n2', 'n3'});
|
||||||
|
});
|
||||||
|
|
||||||
test('mixed spreads the new cards evenly among the due ones', () {
|
test('mixed spreads the new cards evenly among the due ones', () {
|
||||||
final queue = studyQueue(
|
final queue = studyQueue(
|
||||||
groups: [
|
groups: [
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,19 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('saved as they came, the defaults stay the defaults', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
stored('japanese', 'Japanese');
|
||||||
|
await showLibrary(tester);
|
||||||
|
|
||||||
|
await choose(tester, 'Japanese', 'Study options');
|
||||||
|
await tester.tap(find.text('Save'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(gateway.writes, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('go back to the defaults with Reset', (tester) async {
|
testWidgets('go back to the defaults with Reset', (tester) async {
|
||||||
gateway.receiveRemoteChange(
|
gateway.receiveRemoteChange(
|
||||||
'japanese',
|
'japanese',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue