feat(archive): import a studyset with everything in it and its progress
Your call (2026-10-11): an archive, imported from the Library's Import, to
bring a library from elsewhere as one's own, and later to restore backups
and move studysets between instances before federation.
An archive is a .zip: archive.json says its format; the studyset is laid
out as a release (ADR 0005, rule 3), so the release copier writes it, now
also unlinked from any publication (ReleaseCopier.write), its cards marked
"archive" as imported ones are, handing back each item's new id. A card may
name a dictionary entry instead of its payload: the import makes it from
the instance's dictionary as adding from it does, with today's entry,
examples and tags, and leaves out (counting) one the instance lacks; for
that, DictionaryEndpoint.entries gives entries by their ids, at most 1,000
at once. reviews.json lists reviews of the archive's cards; they go into
the person's review log once the cards are in, so every card's schedule
goes on from them.
The sheet says what the archive holds ("Japanese" holds: 12 studysets,
7,745 flashcards, 3 paths, 20,317 reviews), imports once confirmed, busy
meanwhile, and opens the studyset made. Not an archive, a newer format or
an unreadable one says so and imports nothing. No export into the format
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
47743ced49
commit
ff31652a3a
6 changed files with 1548 additions and 37 deletions
|
|
@ -171,6 +171,43 @@ class DictionaryEndpoint extends Endpoint {
|
|||
];
|
||||
}
|
||||
|
||||
/// The entries of the dictionary of [source] with meanings in
|
||||
/// [glossLanguage] by their ids ([sourceIds], at most [maxCompared]), in
|
||||
/// the order named: for cards an archive names by their entry, made as
|
||||
/// they would be from the dictionary. An entry gone from the source isn't
|
||||
/// among them.
|
||||
Future<List<DictionaryHit>> entries(
|
||||
Session session,
|
||||
String source,
|
||||
String glossLanguage,
|
||||
List<String> sourceIds,
|
||||
) async {
|
||||
if (sourceIds.length > maxCompared) {
|
||||
throw ArgumentError.value(
|
||||
sourceIds.length,
|
||||
'sourceIds',
|
||||
'At most $maxCompared at once',
|
||||
);
|
||||
}
|
||||
if (sourceIds.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
final found = {
|
||||
for (final entry in await DictionaryEntry.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(source) &
|
||||
t.glossLanguage.equals(glossLanguage) &
|
||||
t.sourceId.inSet(sourceIds.toSet()),
|
||||
))
|
||||
entry.sourceId: entry,
|
||||
};
|
||||
return [
|
||||
for (final id in sourceIds)
|
||||
if (found[id] case final entry?) _hit(entry),
|
||||
];
|
||||
}
|
||||
|
||||
DictionaryHit _hit(DictionaryEntry entry) => DictionaryHit(
|
||||
source: entry.source,
|
||||
sourceId: entry.sourceId,
|
||||
|
|
|
|||
|
|
@ -572,6 +572,38 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
params['versions'],
|
||||
),
|
||||
),
|
||||
'entries': _is.MethodConnector(
|
||||
name: 'entries',
|
||||
params: {
|
||||
'source': _is.ParameterDescription(
|
||||
name: 'source',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'glossLanguage': _is.ParameterDescription(
|
||||
name: 'glossLanguage',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'sourceIds': _is.ParameterDescription(
|
||||
name: 'sourceIds',
|
||||
type: _is.getType<List<String>>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['dictionary'] as _ilgtwlto.DictionaryEndpoint)
|
||||
.entries(
|
||||
session,
|
||||
params['source'],
|
||||
params['glossLanguage'],
|
||||
params['sourceIds'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['explore'] = _is.EndpointConnector(
|
||||
|
|
|
|||
|
|
@ -3937,6 +3937,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
)
|
||||
as T;
|
||||
}
|
||||
if (t == List<String>) {
|
||||
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
|
||||
}
|
||||
if (t == List<_iscq690j.ExploreEntry>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_iscq690j.ExploreEntry>(e))
|
||||
|
|
@ -3953,9 +3956,6 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<String>) {
|
||||
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
|
||||
}
|
||||
try {
|
||||
return _iais.Protocol().deserialize<T>(data, t);
|
||||
} on _is.DeserializationTypeNotFoundException catch (_) {}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ dictionary:
|
|||
- countLabelled:
|
||||
- labelled:
|
||||
- changed:
|
||||
- entries:
|
||||
explore:
|
||||
- list:
|
||||
- entries:
|
||||
|
|
|
|||
|
|
@ -582,6 +582,44 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('entries: those named, in the order named; none gone from the '
|
||||
'source, nor from another dictionary', () async {
|
||||
await loadDictionaryFile(session, write([taberu, terebi]));
|
||||
final ids = [
|
||||
for (final hit in await endpoints.dictionary.labelled(
|
||||
signedIn,
|
||||
'jmdict',
|
||||
'en',
|
||||
'jlpt:n5',
|
||||
))
|
||||
hit.sourceId,
|
||||
];
|
||||
|
||||
Future<List<String>> entries(List<String> asked) async => [
|
||||
for (final hit in await endpoints.dictionary.entries(
|
||||
signedIn,
|
||||
'jmdict',
|
||||
'en',
|
||||
asked,
|
||||
))
|
||||
hit.sourceId,
|
||||
];
|
||||
|
||||
expect(await entries(ids.reversed.toList()), ids.reversed);
|
||||
expect(await entries([ids.first, '404']), [ids.first]);
|
||||
expect(
|
||||
await endpoints.dictionary.entries(signedIn, 'jmdict', 'de', ids),
|
||||
isEmpty,
|
||||
);
|
||||
expect(await entries([]), isEmpty);
|
||||
expect(
|
||||
() => entries([
|
||||
for (var i = 0; i <= DictionaryEndpoint.maxCompared; i++) '$i',
|
||||
]),
|
||||
throwsA(anything),
|
||||
);
|
||||
});
|
||||
|
||||
test('the dictionaries are for those signed in', () {
|
||||
expect(
|
||||
() => endpoints.dictionary.search(
|
||||
|
|
|
|||
|
|
@ -974,6 +974,43 @@ class _DictionaryEndpoint {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<List<_ism30i9n.DictionaryHit>> entries(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
String source,
|
||||
String glossLanguage,
|
||||
List<String> sourceIds,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'dictionary',
|
||||
method: 'entries',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'dictionary',
|
||||
methodName: 'entries',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'sourceIds': sourceIds,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<_ism30i9n.DictionaryHit>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ExploreEndpoint {
|
||||
|
|
|
|||
Loading…
Reference in a new issue