fix(explore): copies keep their ties to PDF pages, and ids swap whole
A copy's cards, notes and screenshots tied to a page of one of its PDFs kept the author's PDF id, so the tie led nowhere in every copy from Explore and every archive imported. The copier now writes the PDFs first and points what is tied to them at the PDFs it made; an update does the same, writing a release's new PDFs before the cards tied to them, so a card the release changes keeps its tie. The files a copy names were swapped by replacing each author's id in the payload's text, one after another: an id that was another's new one swapped twice, and an id written in the text changed too. Now only whole values are swapped (withOwnIds): a block's asset URL, a PDF's file, a study button's or an exam plan's studysets, a page's PDF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
39926ddc5b
commit
197726f5a3
7 changed files with 284 additions and 103 deletions
File diff suppressed because one or more lines are too long
|
|
@ -191,6 +191,18 @@ String? assetIdOf(Object? url) =>
|
||||||
|
|
||||||
const _assetScheme = 'asset:';
|
const _assetScheme = 'asset:';
|
||||||
|
|
||||||
|
/// The person's files [json] (a payload's JSON, or a part of it) names: the
|
||||||
|
/// `url` of every block that shows or plays one, as [RichContent.assetIds]
|
||||||
|
/// finds them, at any depth.
|
||||||
|
Set<String> assetIdsIn(Object? json) => switch (json) {
|
||||||
|
final Map<String, Object?> map => {
|
||||||
|
?assetIdOf(map['url']),
|
||||||
|
for (final value in map.values) ...assetIdsIn(value),
|
||||||
|
},
|
||||||
|
final List<Object?> list => {for (final value in list) ...assetIdsIn(value)},
|
||||||
|
_ => const {},
|
||||||
|
};
|
||||||
|
|
||||||
/// The attribute holding the text of a block that keeps it beside its
|
/// The attribute holding the text of a block that keeps it beside its
|
||||||
/// blocks, by the block's type.
|
/// blocks, by the block's type.
|
||||||
const _textKeys = {
|
const _textKeys = {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ 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';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/model/card.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/explore/data/release_copier.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_entries.dart';
|
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_entries.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/explore/model/explore_studyset.dart';
|
import 'package:halkyon_learn_flutter/src/feature/explore/model/explore_studyset.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_payload.dart';
|
||||||
|
|
@ -138,13 +139,24 @@ class CopyUpdater(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Map<String, Object?> owned(Map<String, Object?> payload) {
|
// A payload of the release naming the copy's own studysets, PDFs and
|
||||||
var text = jsonEncode(payload);
|
// files, as the copier wrote the copy; those the update brings as soon
|
||||||
for (final MapEntry(key: author, value: person) in own.entries) {
|
// as they are made.
|
||||||
text = text.replaceAll(author, person);
|
Map<String, Object?> owned(Map<String, Object?> payload) =>
|
||||||
}
|
withOwnIds(
|
||||||
return jsonDecode(text) as Map<String, Object?>;
|
payload,
|
||||||
}
|
studysets: {
|
||||||
|
for (final MapEntry(key: item, value: studyset)
|
||||||
|
in copy.studysets.entries)
|
||||||
|
item: studyset.id,
|
||||||
|
},
|
||||||
|
pdfs: {
|
||||||
|
for (final MapEntry(key: item, value: pdf) in copy.pdfs.entries)
|
||||||
|
item: pdf.id,
|
||||||
|
},
|
||||||
|
assets: own,
|
||||||
|
)!
|
||||||
|
as Map<String, Object?>;
|
||||||
|
|
||||||
var toSettle = 0;
|
var toSettle = 0;
|
||||||
void count(UpdateTaken taken) {
|
void count(UpdateTaken taken) {
|
||||||
|
|
@ -266,6 +278,39 @@ class CopyUpdater(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PDFs, before what is tied to their pages.
|
||||||
|
for (final entry in diff.changed.where(_is(ReleaseKind.pdf))) {
|
||||||
|
final pdf = copy.pdfs[entry.id]!;
|
||||||
|
await _pdfs.update(
|
||||||
|
pdf.id,
|
||||||
|
_pdfWithOrigin(
|
||||||
|
decodePdfSourcePayload(
|
||||||
|
_bytes(
|
||||||
|
_threeWay(
|
||||||
|
owned(diff.base[entry.id]!.payload),
|
||||||
|
owned(entry.payload),
|
||||||
|
_json(encodePdfSourcePayload(pdf.content)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pdf.content.published!.taking(version),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (final entry
|
||||||
|
in diff.added.where(_is(ReleaseKind.pdf)).toList()..sort(_byPosition)) {
|
||||||
|
final studysetId = copy.studysets[entry.container]!.id;
|
||||||
|
final content = _pdfWithOrigin(
|
||||||
|
decodePdfSourcePayload(_bytes(owned(entry.payload))),
|
||||||
|
_origin(newer, entry.id),
|
||||||
|
);
|
||||||
|
copy.pdfs[entry.id] = PdfSource(
|
||||||
|
id: await _pdfs.add(studysetId, content),
|
||||||
|
studysetId: studysetId,
|
||||||
|
content: content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Cards.
|
// Cards.
|
||||||
for (final entry in diff.changed.where(_is(ReleaseKind.card))) {
|
for (final entry in diff.changed.where(_is(ReleaseKind.card))) {
|
||||||
final card = copy.cards[entry.id]!;
|
final card = copy.cards[entry.id]!;
|
||||||
|
|
@ -420,36 +465,6 @@ class CopyUpdater(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDFs.
|
|
||||||
for (final entry in diff.changed.where(_is(ReleaseKind.pdf))) {
|
|
||||||
final pdf = copy.pdfs[entry.id]!;
|
|
||||||
await _pdfs.update(
|
|
||||||
pdf.id,
|
|
||||||
_pdfWithOrigin(
|
|
||||||
decodePdfSourcePayload(
|
|
||||||
_bytes(
|
|
||||||
_threeWay(
|
|
||||||
owned(diff.base[entry.id]!.payload),
|
|
||||||
owned(entry.payload),
|
|
||||||
_json(encodePdfSourcePayload(pdf.content)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
pdf.content.published!.taking(version),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (final entry
|
|
||||||
in diff.added.where(_is(ReleaseKind.pdf)).toList()..sort(_byPosition)) {
|
|
||||||
await _pdfs.add(
|
|
||||||
copy.studysets[entry.container]!.id,
|
|
||||||
_pdfWithOrigin(
|
|
||||||
decodePdfSourcePayload(_bytes(owned(entry.payload))),
|
|
||||||
_origin(newer, entry.id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// What the author withdrew stays the learner's, marked.
|
// What the author withdrew stays the learner's, marked.
|
||||||
for (final entry in diff.withdrawn) {
|
for (final entry in diff.withdrawn) {
|
||||||
switch (entry.kind) {
|
switch (entry.kind) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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/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/core/content/rich_content.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';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||||
|
|
@ -79,8 +80,8 @@ class ReleaseCopier(
|
||||||
/// Writes [release]'s items into the library as the person's own, linked
|
/// Writes [release]'s items into the library as the person's own, linked
|
||||||
/// to no publication: an archive's, as a backup or a move brings them
|
/// to no publication: an archive's, as a backup or a move brings them
|
||||||
/// (`feature/archive/`). Returns the top studyset and, by each item's id
|
/// (`feature/archive/`). Returns the top studyset and, by each item's id
|
||||||
/// in the release, the id it has now (the archive's reviews name cards by
|
/// in the release, the id it has now (an archive's progress names its
|
||||||
/// theirs). Otherwise as [copy].
|
/// items by theirs). Otherwise as [copy].
|
||||||
/// Its cards say they came from [cardOrigin] ("archive"), as imported
|
/// Its cards say they came from [cardOrigin] ("archive"), as imported
|
||||||
/// ones do.
|
/// ones do.
|
||||||
Future<({String top, Map<String, String> ids})> write(
|
Future<({String top, Map<String, String> ids})> write(
|
||||||
|
|
@ -167,8 +168,23 @@ class ReleaseCopier(
|
||||||
step();
|
step();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The PDFs before what is tied to their pages.
|
||||||
|
final pdfIds = <String, String>{};
|
||||||
|
for (final directory in read.directories) {
|
||||||
|
for (final pdf in _ordered(read.pdfs[directory])) {
|
||||||
|
pdfIds[pdf.id] = ids[pdf.id] = await _pdfs.add(
|
||||||
|
studysetIds[directory]!,
|
||||||
|
origin == null
|
||||||
|
? pdf.content
|
||||||
|
: _pdfWithOrigin(pdf.content, origin(pdf.id)),
|
||||||
|
);
|
||||||
|
step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Content naming a studyset of the release (a study button) names the
|
// 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.
|
// one made of it now, as it names the person's own files; content tied
|
||||||
|
// to a PDF's page (a card, a note, a screenshot) the PDF made of it.
|
||||||
final named = _read(
|
final named = _read(
|
||||||
release,
|
release,
|
||||||
assetIds,
|
assetIds,
|
||||||
|
|
@ -177,6 +193,7 @@ class ReleaseCopier(
|
||||||
in read.studysets.entries)
|
in read.studysets.entries)
|
||||||
item.id: studysetIds[directory]!,
|
item.id: studysetIds[directory]!,
|
||||||
},
|
},
|
||||||
|
pdfIds: pdfIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
// An exam plan's topics are studysets of the release too: its plans
|
// An exam plan's topics are studysets of the release too: its plans
|
||||||
|
|
@ -249,7 +266,7 @@ class ReleaseCopier(
|
||||||
}
|
}
|
||||||
step();
|
step();
|
||||||
for (final stepItem in _ordered(named.steps[path.id])) {
|
for (final stepItem in _ordered(named.steps[path.id])) {
|
||||||
await _paths.createStep(
|
ids[stepItem.id] = (await _paths.createStep(
|
||||||
pathId,
|
pathId,
|
||||||
stepItem.kind!,
|
stepItem.kind!,
|
||||||
origin == null
|
origin == null
|
||||||
|
|
@ -258,33 +275,24 @@ class ReleaseCopier(
|
||||||
StepField.published,
|
StepField.published,
|
||||||
origin(stepItem.id),
|
origin(stepItem.id),
|
||||||
),
|
),
|
||||||
);
|
)).stepId;
|
||||||
step();
|
step();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final pdf in _ordered(named.pdfs[directory])) {
|
|
||||||
await _pdfs.add(
|
|
||||||
studysetId,
|
|
||||||
origin == null
|
|
||||||
? pdf.content
|
|
||||||
: _pdfWithOrigin(pdf.content, origin(pdf.id)),
|
|
||||||
);
|
|
||||||
step();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return (top: studysetIds['']!, ids: ids);
|
return (top: studysetIds['']!, ids: ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [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, and each studyset of it a
|
/// asset row [assetIds] gives its old one, each studyset of it a study
|
||||||
/// study button names by the one [studysetIds] gives (the content's only
|
/// button or an exam plan names by the one [studysetIds] gives, and each
|
||||||
/// reference to a studyset); throws [ReleaseUnreadable] for an item this
|
/// PDF a page is tied to by the one [pdfIds] gives; throws
|
||||||
/// app can't read.
|
/// [ReleaseUnreadable] for an item this app can't read.
|
||||||
_Read _read(
|
_Read _read(
|
||||||
PublishedRelease release,
|
PublishedRelease release,
|
||||||
Map<String, String> assetIds, {
|
Map<String, String> assetIds, {
|
||||||
Map<String, String> studysetIds = const {},
|
Map<String, String> studysetIds = const {},
|
||||||
|
Map<String, String> pdfIds = const {},
|
||||||
}) {
|
}) {
|
||||||
final read = _Read();
|
final read = _Read();
|
||||||
try {
|
try {
|
||||||
|
|
@ -307,11 +315,18 @@ 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(_renamed(json['payload'], studysetIds));
|
final payload = ByteData.sublistView(
|
||||||
for (final MapEntry(key: old, value: own) in assetIds.entries) {
|
utf8.encode(
|
||||||
text = text.replaceAll(old, own);
|
jsonEncode(
|
||||||
}
|
withOwnIds(
|
||||||
final payload = ByteData.sublistView(utf8.encode(text));
|
json['payload'],
|
||||||
|
studysets: studysetIds,
|
||||||
|
pdfs: pdfIds,
|
||||||
|
assets: assetIds,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
switch (kindAt < 0 ? 'studyset.json' : segments[kindAt]) {
|
switch (kindAt < 0 ? 'studyset.json' : segments[kindAt]) {
|
||||||
case 'studyset.json' when segments.last == 'studyset.json':
|
case 'studyset.json' when segments.last == 'studyset.json':
|
||||||
read.studysets[directory] = _Item(
|
read.studysets[directory] = _Item(
|
||||||
|
|
@ -386,30 +401,6 @@ class ReleaseCopier(
|
||||||
|
|
||||||
static const _kinds = {'cards', 'notes', 'paths', 'pdfs'};
|
static const _kinds = {'cards', 'notes', 'paths', 'pdfs'};
|
||||||
|
|
||||||
/// [json] with every studyset a study button or an exam plan 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,
|
|
||||||
// The topics an exam plan covers.
|
|
||||||
('topics', final List<Object?> ids) => [
|
|
||||||
for (final id in ids)
|
|
||||||
if (id is String) names[id] ?? id else 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(
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -471,6 +462,51 @@ class ReleaseCopier(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A release's [json] (an item's payload, or a part of it) with the ids of
|
||||||
|
/// what it names swapped for the person's own: every studyset a study
|
||||||
|
/// button or an exam plan names by the one [studysets] gives, every PDF a
|
||||||
|
/// page is tied to (a card's or note's `source`, a screenshot's attribute:
|
||||||
|
/// both `pdf`) by the one [pdfs] gives, and every file a block shows or
|
||||||
|
/// plays (its `url`, `asset:<id>`) or a PDF is (its `asset`) by the one
|
||||||
|
/// [assets] gives. Only whole values are swapped, so an id in the text
|
||||||
|
/// stays as written.
|
||||||
|
Object? withOwnIds(
|
||||||
|
Object? json, {
|
||||||
|
required Map<String, String> studysets,
|
||||||
|
required Map<String, String> pdfs,
|
||||||
|
required Map<String, String> assets,
|
||||||
|
}) => switch (json) {
|
||||||
|
_ when studysets.isEmpty && pdfs.isEmpty && assets.isEmpty => json,
|
||||||
|
final List<Object?> list => [
|
||||||
|
for (final item in list)
|
||||||
|
withOwnIds(item, studysets: studysets, pdfs: pdfs, assets: assets),
|
||||||
|
],
|
||||||
|
final Map<String, Object?> map => {
|
||||||
|
for (final MapEntry(:key, :value) in map.entries)
|
||||||
|
key: switch ((key, value)) {
|
||||||
|
(studyButtonStudysetKey, final String id) => studysets[id] ?? id,
|
||||||
|
// The topics an exam plan covers.
|
||||||
|
('topics', final List<Object?> ids) => [
|
||||||
|
for (final id in ids)
|
||||||
|
if (id is String) studysets[id] ?? id else id,
|
||||||
|
],
|
||||||
|
(imagePdfKey, final String id) => pdfs[id] ?? id,
|
||||||
|
('url', final String url) => switch (assets[assetIdOf(url)]) {
|
||||||
|
final own? => assetUrl(own),
|
||||||
|
null => url,
|
||||||
|
},
|
||||||
|
('asset', final String id) => assets[id] ?? id,
|
||||||
|
_ => withOwnIds(
|
||||||
|
value,
|
||||||
|
studysets: studysets,
|
||||||
|
pdfs: pdfs,
|
||||||
|
assets: assets,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => json,
|
||||||
|
};
|
||||||
|
|
||||||
/// A release's items decoded, by the directory of the studyset they are in
|
/// A release's items decoded, by the directory of the studyset they are in
|
||||||
/// (`''` for the top one, `<id>/<id>` further in); steps by their path.
|
/// (`''` for the top one, `<id>/<id>` further in); steps by their path.
|
||||||
final class _Read() {
|
final class _Read() {
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ class FeedbackRepository(
|
||||||
}
|
}
|
||||||
// The files its content names, for the author to take into theirs.
|
// The files its content names, for the author to take into theirs.
|
||||||
final files = <FeedbackFile>[];
|
final files = <FeedbackFile>[];
|
||||||
for (final assetId in _assetIdsIn(proposed)) {
|
for (final assetId in assetIdsIn(proposed)) {
|
||||||
if (await _assets.fileOf(assetId) case final file?) {
|
if (await _assets.fileOf(assetId) case final file?) {
|
||||||
files.add(
|
files.add(
|
||||||
FeedbackFile(
|
FeedbackFile(
|
||||||
|
|
@ -256,14 +256,3 @@ class FeedbackRepository(
|
||||||
jsonDecode(utf8.decode(Uint8List.sublistView(payload)))
|
jsonDecode(utf8.decode(Uint8List.sublistView(payload)))
|
||||||
as Map<String, Object?>;
|
as Map<String, Object?>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The person's files [json] (content's JSON) names: the `url` of every
|
|
||||||
/// block that shows or plays one, as [RichContent.assetIds] finds them.
|
|
||||||
Set<String> _assetIdsIn(Object? json) => switch (json) {
|
|
||||||
final Map<String, Object?> map => {
|
|
||||||
?assetIdOf(map['url']),
|
|
||||||
for (final value in map.values) ..._assetIdsIn(value),
|
|
||||||
},
|
|
||||||
final List<Object?> list => {for (final value in list) ..._assetIdsIn(value)},
|
|
||||||
_ => const {},
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -542,4 +542,70 @@ void main() {
|
||||||
['Animals', 'JLPT-N5', 'Noun'],
|
['Animals', 'JLPT-N5', 'Noun'],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("what is tied to a page of a PDF stays tied to the copy's PDF when "
|
||||||
|
'the release changes it, and what a release ties to a PDF it brings '
|
||||||
|
'is tied to the one made of it', () async {
|
||||||
|
PublishedRelease release(int version) {
|
||||||
|
final base = japanese(version);
|
||||||
|
Map<String, Object?> page(String id, String back, String pdf) =>
|
||||||
|
sortedKeys({
|
||||||
|
'id': id,
|
||||||
|
'position': 'a5',
|
||||||
|
'tags': const <String>[],
|
||||||
|
'payload': json(
|
||||||
|
encodeCardPayload(
|
||||||
|
CardContent(
|
||||||
|
front: RichContent.plain(id),
|
||||||
|
back: RichContent.plain(back),
|
||||||
|
source: (pdfId: pdf, page: 3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return PublishedRelease(
|
||||||
|
publicId: base.publicId,
|
||||||
|
version: version,
|
||||||
|
items: {
|
||||||
|
...base.items,
|
||||||
|
'cards/page.json': page(
|
||||||
|
'page',
|
||||||
|
version == 1 ? 'is' : 'is, politely',
|
||||||
|
'book',
|
||||||
|
),
|
||||||
|
if (version > 1) ...{
|
||||||
|
'pdfs/workbook.json': sortedKeys({
|
||||||
|
'id': 'workbook',
|
||||||
|
'position': 'a1',
|
||||||
|
'payload': json(
|
||||||
|
encodePdfSourcePayload(
|
||||||
|
const PdfSourceContent(
|
||||||
|
title: 'Workbook',
|
||||||
|
fileName: 'workbook.pdf',
|
||||||
|
assetId: bookAsset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'cards/drill.json': page('drill', 'a drill', 'workbook'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final v1 = release(1);
|
||||||
|
final root = await copyOf(v1);
|
||||||
|
await updater.take(root, v1, release(2), fileOf: fileOf);
|
||||||
|
|
||||||
|
final copied = await pdfs.sourcesOf(root);
|
||||||
|
String pdfTitled(String title) =>
|
||||||
|
copied.singleWhere((pdf) => pdf.content.title == title).id;
|
||||||
|
final page = await cardFrom(root, 'page');
|
||||||
|
expect(page.content.back.plainText, 'is, politely');
|
||||||
|
expect(page.content.source, (pdfId: pdfTitled('A book'), page: 3));
|
||||||
|
expect(
|
||||||
|
(await cardFrom(root, 'drill')).content.source,
|
||||||
|
(pdfId: pdfTitled('Workbook'), page: 3),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,22 @@ void main() {
|
||||||
),
|
),
|
||||||
'kanji/cards/pictured.json': item(
|
'kanji/cards/pictured.json': item(
|
||||||
'pictured',
|
'pictured',
|
||||||
encodeCardPayload(card('asset:$imageAsset', '絵')),
|
encodeCardPayload(
|
||||||
|
CardContent(
|
||||||
|
front: RichContent({
|
||||||
|
'document': {
|
||||||
|
'type': 'page',
|
||||||
|
'children': [
|
||||||
|
{
|
||||||
|
'type': 'image',
|
||||||
|
'data': {'url': assetUrl(imageAsset)},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
back: RichContent.plain('絵'),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
'notes/particles.json': item(
|
'notes/particles.json': item(
|
||||||
'particles',
|
'particles',
|
||||||
|
|
@ -318,10 +333,8 @@ void main() {
|
||||||
(studyset) => studyset.parentId == id,
|
(studyset) => studyset.parentId == id,
|
||||||
);
|
);
|
||||||
final pictured = (await cards.cardsOf(kanji.id)).single;
|
final pictured = (await cards.cardsOf(kanji.id)).single;
|
||||||
final front = pictured.content.front.plainText;
|
final assetId = pictured.content.front.assetIds.single;
|
||||||
expect(front, startsWith('asset:'));
|
expect(assetId, isNot(imageAsset));
|
||||||
expect(front, isNot(contains(imageAsset)));
|
|
||||||
final assetId = front.substring('asset:'.length);
|
|
||||||
expect((await assets.fileOf(assetId))!.bytes, image);
|
expect((await assets.fileOf(assetId))!.bytes, image);
|
||||||
final pdf = (await pdfs.sourcesOf(kanji.id)).single;
|
final pdf = (await pdfs.sourcesOf(kanji.id)).single;
|
||||||
expect(pdf.content.title, 'A book');
|
expect(pdf.content.title, 'A book');
|
||||||
|
|
@ -330,6 +343,56 @@ void main() {
|
||||||
expect((await assets.fileOf(pdf.content.assetId))!.bytes, book);
|
expect((await assets.fileOf(pdf.content.assetId))!.bytes, book);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a card or note tied to a page of the release's PDF is tied to the "
|
||||||
|
'PDF made of it', () async {
|
||||||
|
final release = japanese();
|
||||||
|
final id = await copier.copy(
|
||||||
|
PublishedRelease(
|
||||||
|
publicId: release.publicId,
|
||||||
|
version: release.version,
|
||||||
|
items: {
|
||||||
|
...release.items,
|
||||||
|
'kanji/cards/page.json': item(
|
||||||
|
'page',
|
||||||
|
encodeCardPayload(
|
||||||
|
CardContent(
|
||||||
|
front: RichContent.plain('本'),
|
||||||
|
back: RichContent.plain('book'),
|
||||||
|
source: (pdfId: 'book', page: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
position: 'a5',
|
||||||
|
),
|
||||||
|
'kanji/notes/reading.json': item(
|
||||||
|
'reading',
|
||||||
|
encodeNotePayload(
|
||||||
|
NoteContent(
|
||||||
|
body: RichContent.plain('Chapter 1'),
|
||||||
|
source: (pdfId: 'book', page: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
files: release.files,
|
||||||
|
),
|
||||||
|
location: 'halkyon.eu/@alice/japanese',
|
||||||
|
fileOf: fileOf,
|
||||||
|
);
|
||||||
|
|
||||||
|
final kanji = (await studysets.all()).singleWhere(
|
||||||
|
(studyset) => studyset.parentId == id,
|
||||||
|
);
|
||||||
|
final pdf = (await pdfs.sourcesOf(kanji.id)).single;
|
||||||
|
final page = (await cards.cardsOf(
|
||||||
|
kanji.id,
|
||||||
|
)).singleWhere((card) => card.content.front.plainText == '本');
|
||||||
|
expect(page.content.source, (pdfId: pdf.id, page: 2));
|
||||||
|
expect(
|
||||||
|
(await notes.notesOf(kanji.id)).single.content.source,
|
||||||
|
(pdfId: pdf.id, page: 2),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('notes and learning paths, steps in their order', () async {
|
test('notes and learning paths, steps in their order', () async {
|
||||||
final id = await copier.copy(
|
final id = await copier.copy(
|
||||||
japanese(),
|
japanese(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue