fix(explore): a study button in a copy studies the copy's studyset
A study button names the studyset it studies by its id. Copied from a release or an archive, the studysets get new ids, and a button in the content kept naming the author's (or the archive's), which the person doesn't have: it studied nothing. The copier now writes the content after the studysets, each study button naming the studyset made of the one it named; only a study button's studyset, as a short id could also be a word of the content. A later release taken into a copy still brings the author's ids into its buttons: the copy updater doesn't rename them yet. 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:
parent
ff31652a3a
commit
0d4f7a432d
2 changed files with 107 additions and 11 deletions
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
|
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_payload.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/data/card_payload.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||||
|
|
@ -166,14 +167,26 @@ class ReleaseCopier(
|
||||||
step();
|
step();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content naming a studyset of the release (a study button) names the
|
||||||
|
// one made of it now, as it names the person's own files.
|
||||||
|
final named = _read(
|
||||||
|
release,
|
||||||
|
assetIds,
|
||||||
|
studysetIds: {
|
||||||
|
for (final MapEntry(key: directory, value: item)
|
||||||
|
in read.studysets.entries)
|
||||||
|
item.id: studysetIds[directory]!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final tagIds = <String, String>{};
|
final tagIds = <String, String>{};
|
||||||
Future<Set<String>> tagsOf(List<String> names) async => {
|
Future<Set<String>> tagsOf(List<String> names) async => {
|
||||||
for (final name in names) tagIds[name] ??= await _tags.create(name),
|
for (final name in names) tagIds[name] ??= await _tags.create(name),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (final directory in read.directories) {
|
for (final directory in named.directories) {
|
||||||
final studysetId = studysetIds[directory]!;
|
final studysetId = studysetIds[directory]!;
|
||||||
final cards = _ordered(read.cards[directory]);
|
final cards = _ordered(named.cards[directory]);
|
||||||
final cardIds = await _cards.createAll(studysetId, [
|
final cardIds = await _cards.createAll(studysetId, [
|
||||||
for (final card in cards)
|
for (final card in cards)
|
||||||
if (origin == null)
|
if (origin == null)
|
||||||
|
|
@ -191,7 +204,7 @@ class ReleaseCopier(
|
||||||
done += cards.length;
|
done += cards.length;
|
||||||
onProgress?.call(done / total);
|
onProgress?.call(done / total);
|
||||||
|
|
||||||
for (final note in _ordered(read.notes[directory])) {
|
for (final note in _ordered(named.notes[directory])) {
|
||||||
final (:noteId, revisionId: _) = await _notes.create(
|
final (:noteId, revisionId: _) = await _notes.create(
|
||||||
studysetId,
|
studysetId,
|
||||||
origin == null
|
origin == null
|
||||||
|
|
@ -205,7 +218,7 @@ class ReleaseCopier(
|
||||||
step();
|
step();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final path in _ordered(read.paths[directory])) {
|
for (final path in _ordered(named.paths[directory])) {
|
||||||
final pathId = ids[path.id] = await _paths.createPath(
|
final pathId = ids[path.id] = await _paths.createPath(
|
||||||
studysetId,
|
studysetId,
|
||||||
origin == null
|
origin == null
|
||||||
|
|
@ -216,7 +229,7 @@ class ReleaseCopier(
|
||||||
await _tags.setTagsOfPath(pathId, await tagsOf(path.tags));
|
await _tags.setTagsOfPath(pathId, await tagsOf(path.tags));
|
||||||
}
|
}
|
||||||
step();
|
step();
|
||||||
for (final stepItem in _ordered(read.steps[path.id])) {
|
for (final stepItem in _ordered(named.steps[path.id])) {
|
||||||
await _paths.createStep(
|
await _paths.createStep(
|
||||||
pathId,
|
pathId,
|
||||||
stepItem.kind!,
|
stepItem.kind!,
|
||||||
|
|
@ -231,7 +244,7 @@ class ReleaseCopier(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final pdf in _ordered(read.pdfs[directory])) {
|
for (final pdf in _ordered(named.pdfs[directory])) {
|
||||||
await _pdfs.add(
|
await _pdfs.add(
|
||||||
studysetId,
|
studysetId,
|
||||||
origin == null
|
origin == null
|
||||||
|
|
@ -245,9 +258,15 @@ class ReleaseCopier(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [release]'s items decoded, each file of the release named by the
|
/// [release]'s items decoded, each file of the release named by the
|
||||||
/// asset row [assetIds] gives its old one; throws [ReleaseUnreadable] for
|
/// asset row [assetIds] gives its old one, and each studyset of it a
|
||||||
/// an item this app can't read.
|
/// study button names by the one [studysetIds] gives (the content's only
|
||||||
_Read _read(PublishedRelease release, Map<String, String> assetIds) {
|
/// reference to a studyset); throws [ReleaseUnreadable] for an item this
|
||||||
|
/// app can't read.
|
||||||
|
_Read _read(
|
||||||
|
PublishedRelease release,
|
||||||
|
Map<String, String> assetIds, {
|
||||||
|
Map<String, String> studysetIds = const {},
|
||||||
|
}) {
|
||||||
final read = _Read();
|
final read = _Read();
|
||||||
try {
|
try {
|
||||||
for (final MapEntry(key: path, value: json) in release.items.entries) {
|
for (final MapEntry(key: path, value: json) in release.items.entries) {
|
||||||
|
|
@ -269,7 +288,7 @@ class ReleaseCopier(
|
||||||
tag! as String,
|
tag! as String,
|
||||||
];
|
];
|
||||||
// The person's own files in place of the author's.
|
// The person's own files in place of the author's.
|
||||||
var text = jsonEncode(json['payload']);
|
var text = jsonEncode(_renamed(json['payload'], studysetIds));
|
||||||
for (final MapEntry(key: old, value: own) in assetIds.entries) {
|
for (final MapEntry(key: old, value: own) in assetIds.entries) {
|
||||||
text = text.replaceAll(old, own);
|
text = text.replaceAll(old, own);
|
||||||
}
|
}
|
||||||
|
|
@ -348,6 +367,25 @@ class ReleaseCopier(
|
||||||
|
|
||||||
static const _kinds = {'cards', 'notes', 'paths', 'pdfs'};
|
static const _kinds = {'cards', 'notes', 'paths', 'pdfs'};
|
||||||
|
|
||||||
|
/// [json] with every studyset a study button names by the one [names]
|
||||||
|
/// gives in its place.
|
||||||
|
static Object? _renamed(Object? json, Map<String, String> names) =>
|
||||||
|
names.isEmpty
|
||||||
|
? json
|
||||||
|
: switch (json) {
|
||||||
|
final List<Object?> list => [
|
||||||
|
for (final item in list) _renamed(item, names),
|
||||||
|
],
|
||||||
|
final Map<String, Object?> map => {
|
||||||
|
for (final MapEntry(:key, :value) in map.entries)
|
||||||
|
key: switch ((key, value)) {
|
||||||
|
(studyButtonStudysetKey, final String id) => names[id] ?? id,
|
||||||
|
_ => _renamed(value, names),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => json,
|
||||||
|
};
|
||||||
|
|
||||||
/// The directory of the studyset [directory] is in.
|
/// The directory of the studyset [directory] is in.
|
||||||
static String _parentOf(String directory) => switch (directory.lastIndexOf(
|
static String _parentOf(String directory) => switch (directory.lastIndexOf(
|
||||||
'/',
|
'/',
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_importer.dart';
|
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_importer.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
|
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
|
||||||
|
|
@ -12,7 +13,12 @@ import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_copier.dart';
|
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_copier.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_payload.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_step_payload.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/step_content.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||||
|
|
@ -163,13 +169,14 @@ void main() {
|
||||||
late StudysetRepository studysets;
|
late StudysetRepository studysets;
|
||||||
late TagRepository tags;
|
late TagRepository tags;
|
||||||
late ReviewRepository reviews;
|
late ReviewRepository reviews;
|
||||||
|
late LearningPathRepository paths;
|
||||||
late ArchiveImporter importer;
|
late ArchiveImporter importer;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
studysets = StudysetRepository(FakeStudysetGateway());
|
studysets = StudysetRepository(FakeStudysetGateway());
|
||||||
cards = CardRepository(FakeCardGateway());
|
cards = CardRepository(FakeCardGateway());
|
||||||
final notes = NoteRepository(FakeNoteGateway());
|
final notes = NoteRepository(FakeNoteGateway());
|
||||||
final paths = LearningPathRepository(FakeLearningPathGateway());
|
paths = LearningPathRepository(FakeLearningPathGateway());
|
||||||
tags = TagRepository(
|
tags = TagRepository(
|
||||||
FakeTagGateway(),
|
FakeTagGateway(),
|
||||||
cardChanges: cards.changes,
|
cardChanges: cards.changes,
|
||||||
|
|
@ -203,6 +210,57 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('content naming a studyset of the archive, as a study button '
|
||||||
|
'does, names it as it is in the library', () async {
|
||||||
|
final archive = {
|
||||||
|
...japanese(),
|
||||||
|
'paths/drill/path.json': {
|
||||||
|
'id': 'drill',
|
||||||
|
'position': 'a0',
|
||||||
|
'payload': json(
|
||||||
|
encodeLearningPathPayload(LearningPathContent.titled('Drill')),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'paths/drill/steps/go.json': {
|
||||||
|
'id': 'go',
|
||||||
|
'position': 'a0',
|
||||||
|
'kind': StepKind.info.value,
|
||||||
|
'payload': json(
|
||||||
|
encodeStepPayload(
|
||||||
|
StepContent.empty(StepKind.info).withField(
|
||||||
|
StepField.body,
|
||||||
|
RichContent({
|
||||||
|
'document': {
|
||||||
|
'type': 'page',
|
||||||
|
'children': [
|
||||||
|
{
|
||||||
|
'type': studyButtonBlockType,
|
||||||
|
'data': {studyButtonStudysetKey: 'vocabulary'},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await importer.import(readArchive(zip(archive)));
|
||||||
|
|
||||||
|
final vocabulary = (await studysets.all()).singleWhere(
|
||||||
|
(s) => s.content.title == 'Vocabulary',
|
||||||
|
);
|
||||||
|
final path = (await paths.pathsOf(
|
||||||
|
(await studysets.all()).singleWhere((s) => s.parentId == null).id,
|
||||||
|
)).single;
|
||||||
|
final step = (await paths.stepsOf(path.id)).single;
|
||||||
|
expect(
|
||||||
|
jsonEncode(step.content.body.toJson()),
|
||||||
|
contains('"$studyButtonStudysetKey":"${vocabulary.id}"'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("brings the studysets and cards in as the person's own, a word "
|
test("brings the studysets and cards in as the person's own, a word "
|
||||||
'from the dictionary as adding it makes it, the reviews onto the '
|
'from the dictionary as adding it makes it, the reviews onto the '
|
||||||
'cards they were of; an entry the instance lacks is left out and '
|
'cards they were of; an entry the instance lacks is left out and '
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue