feat(studyset): a studyset opens on the tab it was left on
All checks were successful
CI / check (push) Successful in 14m17s
CI / deploy (push) Successful in 18s

A studyset opened on its flashcards and slid over to its paths once it
had worked out that one was unfinished, every time it was opened. It now
opens on the tab it was left on, on this device: kept in the person's
folder (studyset_tabs.json), read as their data opens, so at once. The
first time a studyset opens here it still chooses by what it holds
(paths in progress, then flashcards, notes, files), but shows that it is
loading until it knows, rather than one tab and then another; paths that
can't be read count as none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-11 23:30:47 +02:00
commit 6b1ff4122e
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
9 changed files with 393 additions and 145 deletions

View file

@ -74,6 +74,7 @@ import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.d
import 'package:halkyon_learn_flutter/src/feature/study/data/sync_review_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/sync_studyset_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/sync_tag_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
@ -364,6 +365,10 @@ Future<PersonDependencies> _openPerson(
ServerpodPublishGateway(client),
FilePublishedStore(File(p.join(folder.path, 'published.json'))),
);
final studysetTabs = StudysetTabs(
FileStudysetTabStore(File(p.join(folder.path, 'studyset_tabs.json'))),
);
await studysetTabs.open();
return PersonDependencies(
sync: sync,
@ -386,6 +391,7 @@ Future<PersonDependencies> _openPerson(
pomodoroRuns: FilePomodoroRunStore(
File(p.join(folder.path, 'pomodoro.json')),
),
studysetTabs: studysetTabs,
profile: ProfileRepository(ServerpodProfileGateway(client)),
publishing: publishing,
explore: ExploreRepository(

View file

@ -18,6 +18,7 @@ import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_reposit
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
/// Everything that belongs to one signed-in person: their local database, its
@ -80,6 +81,9 @@ class PersonDependencies({
/// Where this device keeps the person's Pomodoro timer.
required final PomodoroRunStore pomodoroRuns,
/// The tab each of the person's studysets was left on, on this device.
required final StudysetTabs studysetTabs,
/// The person's public profile, on the instance.
required final ProfileRepository profile,

View file

@ -0,0 +1,78 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:halkyon_learn_flutter/src/core/async/serial_queue.dart';
/// Where the device keeps the tab each of the person's studysets was left
/// on.
abstract interface class StudysetTabStore() {
/// The tabs kept, by studyset id; none when nothing is or it can't be
/// read.
Future<Map<String, String>> read();
/// Keeps [tabs].
Future<void> write(Map<String, String> tabs);
}
/// Keeps them as a JSON object in [_file], in the person's folder, which
/// goes with them; written to a temporary file first and renamed into
/// place.
class FileStudysetTabStore(final File _file) implements StudysetTabStore {
// One write at a time: they share the temporary file.
final _queue = SerialQueue();
@override
Future<Map<String, String>> read() async {
try {
final json = jsonDecode(await _file.readAsString()) as Map;
return {
for (final MapEntry(:key, :value) in json.entries)
if ((key, value) case (final String id, final String tab)) id: tab,
};
} on FileSystemException {
return const {};
} on Object catch (error) {
log(
"The studysets' tabs are unreadable",
name: 'Studyset',
error: error,
);
return const {};
}
}
@override
Future<void> write(Map<String, String> tabs) => _queue.run(() async {
await _file.parent.create(recursive: true);
final temporary = File('${_file.path}.new');
await temporary.writeAsString(jsonEncode(tabs), flush: true);
await temporary.rename(_file.path);
});
}
/// The tab each of the person's studysets was left on, on this device
/// (decided 2026-10-11), so a studyset opens again where it was left rather
/// than choosing anew each time: read once as the person's data opens,
/// kept as tabs are shown. A tab is kept by its name; one the app no longer
/// has reads as none.
class StudysetTabs(final StudysetTabStore _store) {
var _tabs = <String, String>{};
/// Reads the tabs kept, before any studyset opens.
Future<void> open() async => _tabs = {...await _store.read()};
/// The tab the studyset [studysetId] was left on; null if it never opened
/// here.
String? of(String studysetId) => _tabs[studysetId];
/// Keeps [tab] as the one the studyset [studysetId] was left on; done once
/// it is written.
Future<void> keep(String studysetId, String tab) async {
if (_tabs[studysetId] == tab) {
return;
}
_tabs = {..._tabs, studysetId: tab};
await _store.write(_tabs);
}
}

View file

@ -19,6 +19,7 @@ import 'package:halkyon_learn_flutter/src/core/sync/widget/sync_and_wait.dart';
import 'package:halkyon_learn_flutter/src/core/theme/brand.dart';
import 'package:halkyon_learn_flutter/src/core/theme/palette_color.dart';
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
import 'package:halkyon_learn_flutter/src/core/widget/reorder_toggle.dart';
import 'package:halkyon_learn_flutter/src/core/widget/scroll_hiding_fab.dart';
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
@ -54,6 +55,7 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dar
import 'package:halkyon_learn_flutter/src/feature/study/widget/start_studying.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/content_tab_cubit.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/delete_studyset.dart';
@ -605,12 +607,15 @@ class const _BibliographyButton({required final Studyset studyset})
/// narrowing them to a tag and managing tags, on the paths reordering them
/// once there are two.
///
/// It opens on the first tab with something of the studyset's own in it
/// (its topics' content is in theirs), in their order: paths not completed
/// yet, flashcards, notes, files; on the flashcards when there is nothing
/// (decided 2026-10-03). Material added or imported shows in its own tab
/// ([ContentTabCubit]). Once the person chose a tab, or material was added,
/// opening no longer moves it.
/// It opens on the tab it was left on, on this device ([StudysetTabs],
/// decided 2026-10-11). Opened here for the first time, it opens on the
/// first tab with something of the studyset's own in it (its topics'
/// content is in theirs), in their order: paths not completed yet,
/// flashcards, notes, files; on the flashcards when there is nothing
/// (decided 2026-10-03); until it knows which, it shows that it is loading
/// rather than one tab and then another. Material added or imported shows in
/// its own tab ([ContentTabCubit]). Once the person chose a tab, or material
/// was added, opening no longer moves it.
class const _Content({required final String studysetId})
extends StatefulWidget {
@override
@ -623,11 +628,15 @@ class _ContentState()
late final _tabs = TabController(
length: StudysetTab.values.length,
vsync: this,
initialIndex: StudysetTab.flashcards.index,
initialIndex: _active,
);
/// Whether the tab shown is settled: chosen on opening, by the person, or
/// for material added.
/// The tab each studyset was left on, on this device.
late final StudysetTabs _kept = PersonScope.of(context).studysetTabs;
/// Whether the tab shown is settled: the one it was left on, chosen on
/// opening, by the person, or for material added. Until then, no tab
/// shows.
var _settled = false;
/// Whether the paths show handles to drag them.
@ -652,14 +661,30 @@ class _ContentState()
@override
void initState() {
super.initState();
if (StudysetTab.values.asNameMap()[_kept.of(widget.studysetId)]
case final tab?) {
_active = tab.index;
_settled = true;
}
_tabs.addListener(() {
// Swiping to a tab is choosing it as much as tapping it is.
_settled = true;
_settle();
if (!_tabs.indexIsChanging) {
_active = _tabs.index;
unawaited(
_kept.keep(widget.studysetId, StudysetTab.values[_active].name),
);
}
});
unawaited(_readWhatItHolds());
if (!_settled) {
unawaited(_readWhatItHolds());
}
}
void _settle() {
if (!_settled && mounted) {
setState(() => _settled = true);
}
}
@override
@ -695,21 +720,30 @@ class _ContentState()
pdfs: pdfs.containsKey(widget.studysetId),
);
} on Object catch (error) {
// It stays on the flashcards, as it opened.
log(
'Reading what a studyset holds failed',
name: 'Studyset',
error: error,
);
if (mounted && !_settled) {
_show(StudysetTab.flashcards, opening: true);
}
return;
}
_chooseOnOpening();
}
void _show(StudysetTab tab) {
_settled = true;
/// Shows [tab]: at once while [opening], sliding over otherwise.
void _show(StudysetTab tab, {bool opening = false}) {
_settle();
if (_tabs.index != tab.index) {
_tabs.animateTo(tab.index);
if (opening) {
_tabs.index = tab.index;
} else {
_tabs.animateTo(tab.index);
}
} else {
unawaited(_kept.keep(widget.studysetId, tab.name));
}
}
@ -720,14 +754,16 @@ class _ContentState()
return;
}
final paths = context.read<PathListBloc>().state;
// Paths that can't be read are as good as none.
final pathsKnown = paths.completionKnown || paths.failed;
if (paths.completionKnown && paths.hasOpenPaths) {
_show(StudysetTab.paths);
} else if (paths.completionKnown && _holds != null) {
_show(StudysetTab.paths, opening: true);
} else if (pathsKnown && _holds != null) {
_show(switch (_holds!) {
(cards: false, notes: true, pdfs: _) => StudysetTab.notes,
(cards: false, notes: false, pdfs: true) => StudysetTab.files,
_ => StudysetTab.flashcards,
});
}, opening: true);
}
}
@ -783,148 +819,150 @@ class _ContentState()
},
),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LayoutBuilder(
builder: (context, constraints) => Row(
child: !_settled
? const Center(child: AppLoadingIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: TabBar(
controller: _tabs,
onTap: _tapped,
isScrollable: true,
tabAlignment: TabAlignment.start,
indicatorSize: TabBarIndicatorSize.label,
// The line below runs the card's whole width instead.
dividerColor: Colors.transparent,
tabs: [
for (final tab in StudysetTab.values)
Tab(
text: switch (tab) {
StudysetTab.paths => l10n.studysetTabPaths,
StudysetTab.flashcards =>
l10n.studysetTabFlashcards,
StudysetTab.notes => l10n.studysetTabNotes,
StudysetTab.files => l10n.studysetTabFiles,
},
LayoutBuilder(
builder: (context, constraints) => Row(
children: [
Expanded(
child: TabBar(
controller: _tabs,
onTap: _tapped,
isScrollable: true,
tabAlignment: TabAlignment.start,
indicatorSize: TabBarIndicatorSize.label,
// The line below runs the card's whole width instead.
dividerColor: Colors.transparent,
tabs: [
for (final tab in StudysetTab.values)
Tab(
text: switch (tab) {
StudysetTab.paths => l10n.studysetTabPaths,
StudysetTab.flashcards =>
l10n.studysetTabFlashcards,
StudysetTab.notes => l10n.studysetTabNotes,
StudysetTab.files => l10n.studysetTabFiles,
},
),
],
),
),
AnimatedBuilder(
animation: _tabs,
builder: (context, _) => switch (StudysetTab
.values[_tabs.index]) {
StudysetTab.flashcards => Padding(
padding: const EdgeInsets.only(right: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_Browse(
studysetId: widget.studysetId,
labelled: constraints.maxWidth >= 480,
),
const CardListToolbar(),
],
),
),
StudysetTab.paths =>
BlocSelector<PathListBloc, PathListState, int>(
selector: (state) => state.paths.length,
builder: (context, count) => _ReorderBeside(
count: count,
reordering: _reorderingPaths,
// Below that, an icon alone.
labelled: constraints.maxWidth >= 480,
tooltip: l10n.studysetReorderPaths,
onToggle: () => setState(
() => _reorderingPaths = !_reorderingPaths,
),
),
),
StudysetTab.notes =>
BlocSelector<NoteListBloc, NoteListState, int>(
selector: (state) => state.notes.length,
builder: (context, count) => _ReorderBeside(
count: count,
reordering: _reorderingNotes,
labelled: constraints.maxWidth >= 480,
tooltip: l10n.studysetReorderNotes,
onToggle: () => setState(
() => _reorderingNotes = !_reorderingNotes,
),
),
),
StudysetTab.files =>
BlocSelector<PdfListBloc, PdfListState, int>(
selector: (state) => state.rows.length,
builder: (context, count) => _ReorderBeside(
count: count,
reordering: _reorderingFiles,
labelled: constraints.maxWidth >= 480,
tooltip: l10n.studysetReorderFiles,
onToggle: () {
if (!_reorderingFiles) {
// What is dragged is the person's own
// order, whatever the files were sorted by.
context.read<PdfListBloc>().add(
const PdfListEventSorted(PdfSort.custom),
);
}
setState(
() => _reorderingFiles = !_reorderingFiles,
);
},
),
),
},
),
],
),
),
AnimatedBuilder(
animation: _tabs,
builder: (context, _) =>
switch (StudysetTab.values[_tabs.index]) {
StudysetTab.flashcards => Padding(
padding: const EdgeInsets.only(right: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_Browse(
studysetId: widget.studysetId,
labelled: constraints.maxWidth >= 480,
),
const CardListToolbar(),
],
),
const Divider(height: 1),
Expanded(
child: TabBarView(
controller: _tabs,
children: [
_tab(
StudysetTab.paths,
SliverPathList(
studysetId: widget.studysetId,
reordering: _reorderingPaths,
padding: _listPadding,
),
StudysetTab.paths =>
BlocSelector<PathListBloc, PathListState, int>(
selector: (state) => state.paths.length,
builder: (context, count) => _ReorderBeside(
count: count,
reordering: _reorderingPaths,
// Below that, an icon alone.
labelled: constraints.maxWidth >= 480,
tooltip: l10n.studysetReorderPaths,
onToggle: () => setState(
() => _reorderingPaths = !_reorderingPaths,
),
),
),
StudysetTab.notes =>
BlocSelector<NoteListBloc, NoteListState, int>(
selector: (state) => state.notes.length,
builder: (context, count) => _ReorderBeside(
count: count,
reordering: _reorderingNotes,
labelled: constraints.maxWidth >= 480,
tooltip: l10n.studysetReorderNotes,
onToggle: () => setState(
() => _reorderingNotes = !_reorderingNotes,
),
),
),
StudysetTab.files =>
BlocSelector<PdfListBloc, PdfListState, int>(
selector: (state) => state.rows.length,
builder: (context, count) => _ReorderBeside(
count: count,
reordering: _reorderingFiles,
labelled: constraints.maxWidth >= 480,
tooltip: l10n.studysetReorderFiles,
onToggle: () {
if (!_reorderingFiles) {
// What is dragged is the person's own
// order, whatever the files were sorted by.
context.read<PdfListBloc>().add(
const PdfListEventSorted(PdfSort.custom),
);
}
setState(
() => _reorderingFiles = !_reorderingFiles,
);
},
),
),
},
),
],
),
),
const Divider(height: 1),
Expanded(
child: TabBarView(
controller: _tabs,
children: [
_tab(
StudysetTab.paths,
SliverPathList(
studysetId: widget.studysetId,
reordering: _reorderingPaths,
padding: _listPadding,
),
),
_tab(
StudysetTab.flashcards,
SliverCardList(
cards: PersonScope.of(context).cards,
studysetId: widget.studysetId,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 96),
),
),
_tab(
StudysetTab.notes,
SliverNoteList(
studysetId: widget.studysetId,
reordering: _reorderingNotes,
padding: _listPadding,
),
),
_tab(
StudysetTab.files,
SliverPdfList(
studysetId: widget.studysetId,
reordering: _reorderingFiles,
padding: _listPadding,
),
_tab(
StudysetTab.flashcards,
SliverCardList(
cards: PersonScope.of(context).cards,
studysetId: widget.studysetId,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 96),
),
),
_tab(
StudysetTab.notes,
SliverNoteList(
studysetId: widget.studysetId,
reordering: _reorderingNotes,
padding: _listPadding,
),
),
_tab(
StudysetTab.files,
SliverPdfList(
studysetId: widget.studysetId,
reordering: _reorderingFiles,
padding: _listPadding,
),
),
],
),
),
],
),
),
],
),
);
}
}

View file

@ -25,6 +25,7 @@ import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_reposit
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
import 'package:material_ui/material_ui.dart';
@ -46,6 +47,7 @@ import '../../support/fake_tag_gateway.dart';
import '../../support/memory_asset_file_cache.dart';
import '../../support/memory_pomodoro_run_store.dart';
import '../../support/memory_published_store.dart';
import '../../support/memory_studyset_tab_store.dart';
import '../../support/test_dependencies.dart';
/// A person's dependencies on fakes, closing with [onDispose].
@ -80,6 +82,7 @@ PersonDependencies personDependencies({Future<void> Function()? onDispose}) {
stepDeletions: StepDeletions(paths),
pdfDeletions: PdfDeletions(pdfSources),
pomodoroRuns: MemoryPomodoroRunStore(),
studysetTabs: StudysetTabs(MemoryStudysetTabStore()),
profile: ProfileRepository(FakeProfileGateway()),
publishing: PublishRepository(
FakePublishGateway(),

View file

@ -17,6 +17,7 @@ import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/confetti_
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:material_ui/material_ui.dart';
@ -25,6 +26,7 @@ import '../../../support/fake_learning_path_gateway.dart';
import '../../../support/fake_note_gateway.dart';
import '../../../support/fake_path_answer_gateway.dart';
import '../../../support/fake_studyset_gateway.dart';
import '../../../support/memory_studyset_tab_store.dart';
import '../../../support/pump_app.dart';
import '../../../support/test_dependencies.dart';
@ -57,6 +59,7 @@ void main() {
String location, {
DeviceSettings deviceSettings = const DeviceSettings(),
Size size = const Size(1200, 1000),
StudysetTabStore? tabs,
}) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
@ -70,6 +73,7 @@ void main() {
noteGateway: noteGateway,
files: files,
deviceSettings: deviceSettings,
studysetTabStore: tabs,
),
location: location,
);
@ -116,6 +120,43 @@ void main() {
expect(find.text('Particles'), findsOneWidget);
});
testWidgets('a studyset opens again on the tab it was left on, here, '
'even with a path unfinished', (tester) async {
final paths = repository();
final id = await paths.createPath(
'japanese',
LearningPathContent.titled('Particles'),
);
await paths.createStep(id, StepKind.info, step(StepKind.info, 'Read'));
await showApp(
tester,
RoutePaths.studyset('japanese'),
tabs: MemoryStudysetTabStore({'japanese': 'notes'}),
);
expect(shownTab(tester), 2);
});
testWidgets('the tab a studyset shows, chosen on opening or by the '
'person, is kept for the next time', (tester) async {
final paths = repository();
final id = await paths.createPath(
'japanese',
LearningPathContent.titled('Particles'),
);
await paths.createStep(id, StepKind.info, step(StepKind.info, 'Read'));
final tabs = MemoryStudysetTabStore();
await showApp(tester, RoutePaths.studyset('japanese'), tabs: tabs);
expect(shownTab(tester), 0);
expect(tabs.kept, {'japanese': 'paths'});
await tester.tap(find.widgetWithText(Tab, 'Notes'));
await tester.pumpAndSettle();
expect(tabs.kept, {'japanese': 'notes'});
});
testWidgets('a studyset with only notes opens on them', (tester) async {
final notes = NoteRepository(noteGateway);
addTearDown(notes.dispose);

View file

@ -0,0 +1,53 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import '../../../support/memory_studyset_tab_store.dart';
void main() {
late Directory folder;
setUp(() async {
folder = await Directory.systemTemp.createTemp('studyset_tabs');
});
tearDown(() => folder.delete(recursive: true));
test('keeps the tab each studyset was left on, in a file that a new '
'session reads again', () async {
final file = File('${folder.path}/studyset_tabs.json');
final tabs = StudysetTabs(FileStudysetTabStore(file));
await tabs.open();
expect(tabs.of('japanese'), isNull);
await tabs.keep('japanese', 'paths');
await tabs.keep('spanish', 'notes');
await tabs.keep('japanese', 'flashcards');
final later = StudysetTabs(FileStudysetTabStore(file));
await later.open();
expect(later.of('japanese'), 'flashcards');
expect(later.of('spanish'), 'notes');
});
test('reads an unreadable file as no tabs kept', () async {
final file = File('${folder.path}/studyset_tabs.json')
..writeAsStringSync('{not json');
final tabs = StudysetTabs(FileStudysetTabStore(file));
await tabs.open();
expect(tabs.of('japanese'), isNull);
});
test('writes only when the tab changes', () async {
final kept = {'japanese': 'paths'};
final store = MemoryStudysetTabStore(kept);
final tabs = StudysetTabs(store);
await tabs.open();
await tabs.keep('japanese', 'paths');
expect(store.kept, same(kept));
await tabs.keep('japanese', 'notes');
expect(store.kept, {'japanese': 'notes'});
});
}

View file

@ -0,0 +1,16 @@
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
/// Keeps the studysets' tabs in memory.
class MemoryStudysetTabStore([Map<String, String> initial = const {}])
implements StudysetTabStore {
var _tabs = initial;
/// What is kept now.
Map<String, String> get kept => _tabs;
@override
Future<Map<String, String>> read() async => _tabs;
@override
Future<void> write(Map<String, String> tabs) async => _tabs = tabs;
}

View file

@ -69,6 +69,7 @@ import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.d
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
@ -101,6 +102,7 @@ import 'memory_asset_file_cache.dart';
import 'memory_device_settings_store.dart';
import 'memory_pomodoro_run_store.dart';
import 'memory_published_store.dart';
import 'memory_studyset_tab_store.dart';
/// The app's dependencies with fakes instead of the server and the device's
/// database; each test passes in only the fakes it looks at.
@ -147,6 +149,7 @@ Dependencies testDependencies({
ProfileGateway? profileGateway,
PublishGateway? publishGateway,
PublishedStore? publishedStore,
StudysetTabStore? studysetTabStore,
ExploreGateway? exploreGateway,
FeedbackWriteGateway? feedbackWriteGateway,
FeedbackServerGateway? feedbackServerGateway,
@ -238,6 +241,10 @@ Dependencies testDependencies({
final pathDeletions = LearningPathDeletions(paths);
final stepDeletions = StepDeletions(paths);
final pdfDeletions = PdfDeletions(pdfSources);
final studysetTabs = StudysetTabs(
studysetTabStore ?? MemoryStudysetTabStore(),
);
await studysetTabs.open();
return PersonDependencies(
sync: sync,
studysets: studysets,
@ -257,6 +264,7 @@ Dependencies testDependencies({
stepDeletions: stepDeletions,
pdfDeletions: pdfDeletions,
pomodoroRuns: pomodoroRuns ?? MemoryPomodoroRunStore(),
studysetTabs: studysetTabs,
// The picture as it is: drawing it anew is real work a widget
// test's fake clock can't wait for (avatar_image_test.dart has it).
profile: ProfileRepository(