feat(archive): export a studyset as an archive to import again
Export content now asks what to export as: an archive first, a .zip the Library's Import takes back as the person's own, for a backup or a move to another instance; Markdown, to read elsewhere, below it. The archive holds the studyset with everything inside it but what waits in Recently deleted, laid out as the instance writes a release, unlinked from Explore; dictionary cards go whole, with the person's edits. Every file its content names is downloaded first, and an archive missing one is not kept: the person is told how many and to try again online. With my progress (ticked at first) adds the reviews, the answers to learning paths' steps, the pages read in PDFs, and what a release leaves out as only its author's: the exam prepared for, a card's report, a PDF's reading deadline. Stars, archived studysets and the tags' colours come along either way. The import of v0.17.0 refuses a file it doesn't know, so what it doesn't read goes into the files it reads, as keys it skips, and the format stays 1: archive.json's tags, a studyset.json's archived and starred, a step's answers, a PDF's readings. This import reads them; an archive it can't read all of still writes nothing, its tags included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
197726f5a3
commit
6f3ba7b421
1 changed files with 1671 additions and 30 deletions
|
|
@ -16,6 +16,7 @@ import 'package:halkyon_learn_flutter/src/feature/explore/data/release_copier.da
|
|||
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_step_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.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';
|
||||
|
|
@ -34,6 +35,7 @@ import '../../support/fake_card_gateway.dart';
|
|||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_learning_path_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
import '../../support/fake_path_answer_gateway.dart';
|
||||
import '../../support/fake_pdf_source_gateway.dart';
|
||||
import '../../support/fake_review_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
|
|
@ -161,6 +163,90 @@ void main() {
|
|||
),
|
||||
refused(ArchiveProblem.unreadable),
|
||||
);
|
||||
for (final unreadable in <Map<String, Object>>[
|
||||
{
|
||||
'archive.json': {'format': 1, 'tags': 'Kana'},
|
||||
},
|
||||
{
|
||||
'pdfs/book.json': {
|
||||
'id': 'book',
|
||||
'readings': [
|
||||
{'readAt': 'yesterday', 'kind': 'reached', 'page': 3},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
'paths/drill/steps/go.json': {
|
||||
'id': 'go',
|
||||
'answers': [
|
||||
{'answeredAt': '2026-06-09T10:00:00Z', 'outcome': 1},
|
||||
],
|
||||
},
|
||||
},
|
||||
]) {
|
||||
expect(
|
||||
() => readArchive(zip({...japanese(), ...unreadable})),
|
||||
refused(ArchiveProblem.unreadable),
|
||||
reason: '$unreadable',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('reads the progress kept where v0.17.0 leaves it alone: tags in '
|
||||
'their colours, archived and starred studysets, answers to steps '
|
||||
'and pages read', () {
|
||||
final archive = readArchive(
|
||||
zip({
|
||||
...japanese(),
|
||||
'archive.json': {
|
||||
'format': 1,
|
||||
'tags': [
|
||||
{'name': 'Kana', 'color': 'copper'},
|
||||
{'name': 'あかね噺 S1 E1', 'color': 'cobalt', 'origin': 'jellyfin'},
|
||||
],
|
||||
},
|
||||
'vocabulary/studyset.json': {
|
||||
...japanese()['vocabulary/studyset.json']! as Map<String, Object?>,
|
||||
'archived': true,
|
||||
'starred': true,
|
||||
},
|
||||
'pdfs/book.json': {
|
||||
'id': 'book',
|
||||
'readings': [
|
||||
{'readAt': '2026-06-09T10:00:00Z', 'kind': 'reached', 'page': 3},
|
||||
],
|
||||
},
|
||||
'paths/drill/steps/go.json': {
|
||||
'id': 'go',
|
||||
'answers': [
|
||||
{'answeredAt': '2026-06-09T11:00:00Z', 'outcome': 'continued'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(archive.tags, [
|
||||
(name: 'Kana', colorName: 'copper', fromJellyfin: false),
|
||||
(name: 'あかね噺 S1 E1', colorName: 'cobalt', fromJellyfin: true),
|
||||
]);
|
||||
expect(archive.archived, {'vocabulary'});
|
||||
expect(archive.starred, {'vocabulary'});
|
||||
expect(archive.readings, [
|
||||
(
|
||||
pdf: 'book',
|
||||
readAt: DateTime.utc(2026, 6, 9, 10),
|
||||
kind: 'reached',
|
||||
page: 3,
|
||||
),
|
||||
]);
|
||||
expect(archive.answers, [
|
||||
(
|
||||
step: 'go',
|
||||
answeredAt: DateTime.utc(2026, 6, 9, 11),
|
||||
outcome: 'continued',
|
||||
answer: null,
|
||||
),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -207,9 +293,40 @@ void main() {
|
|||
),
|
||||
),
|
||||
reviews,
|
||||
PathProgressRepository(FakePathAnswerGateway()),
|
||||
studysets,
|
||||
pdfs,
|
||||
);
|
||||
});
|
||||
|
||||
test('an archive this app cannot read all of writes nothing, not even '
|
||||
'the tags it lists', () async {
|
||||
final archive = readArchive(
|
||||
zip({
|
||||
...japanese(),
|
||||
'archive.json': {
|
||||
'format': 1,
|
||||
'tags': [
|
||||
{'name': 'Kana', 'color': 'copper'},
|
||||
],
|
||||
},
|
||||
'cards/future.json': {
|
||||
'id': 'future',
|
||||
'position': 'a9',
|
||||
'tags': ['Kana'],
|
||||
'payload': {'schemaVersion': cardSchemaVersion + 1},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
importer.import(archive),
|
||||
throwsA(isA<ReleaseUnreadable>()),
|
||||
);
|
||||
expect((await tags.read()).all, isEmpty);
|
||||
expect(await studysets.all(), isEmpty);
|
||||
});
|
||||
|
||||
test('content naming a studyset of the archive, as a study button '
|
||||
'does, names it as it is in the library', () async {
|
||||
final archive = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue