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:
Tizian 「ティツィアーン」 2026-10-07 03:23:24 +02:00
commit 6ba229be2c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
6 changed files with 60 additions and 4 deletions

View file

@ -122,7 +122,7 @@ class StudySessionBloc(
for (final group in groups) [for (final card in group) card.id],
];
final queue = _queue(schedules, now);
_round = _ordered(queue).take(_options.cardsPerRound).toList();
_round = queue.take(_options.cardsPerRound).toList();
if (_round.isEmpty) {
final upcoming = [
for (final schedule in schedules.values) ?schedule.due,
@ -140,6 +140,9 @@ class StudySessionBloc(
groups: _groupIds,
options: _groupOptions,
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,
now: now,
startOfToday: startOfLocalDay(now),

View file

@ -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
/// [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
/// first, or the new ones spread among the due. When nothing else is due, a
/// learning card due within [learnAhead] comes early.
/// first, or the new ones spread among the due; with [shuffle], the due
/// 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({
required List<List<String>> groups,
required Map<String, CardSchedule> schedules,
@ -26,6 +28,7 @@ List<String> studyQueue({
List<StudysetStudyOptions> options = const [],
int newPerDay = 20,
NewCardOrder order = NewCardOrder.reviewsFirst,
math.Random? shuffle,
Duration learnAhead = const Duration(minutes: 20),
}) {
DateTime? dueOf(String id) => schedules[id]?.due;
@ -68,6 +71,10 @@ List<String> studyQueue({
);
}
due.sort(byDue);
if (shuffle != null) {
due.shuffle(shuffle);
fresh.shuffle(shuffle);
}
final queue = switch (order) {
NewCardOrder.reviewsFirst => [...due, ...fresh],
NewCardOrder.newFirst => [...fresh, ...due],

View file

@ -64,7 +64,13 @@ class _StudyOptionsSheetState() extends State<_StudyOptionsSheet> {
),
const Spacer(),
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),
),
],

View file

@ -277,6 +277,13 @@ class StudysetListBloc(
_paths.changes,
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(
_deletions.watchPending(),
onData: (pending) {

View file

@ -1,3 +1,5 @@
import 'dart:math' as math;
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/study_queue.dart';
@ -196,6 +198,24 @@ void main() {
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', () {
final queue = studyQueue(
groups: [

View file

@ -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 {
gateway.receiveRemoteChange(
'japanese',