feat(jellyfin): watch from one's own Jellyfin server and learn the words of its subtitles
Animelon's way of learning from video, from the person's own Jellyfin server: connected per device (Settings, or a studyset's Add material › Watch from Jellyfin) with a name and password or a Quick Connect code, over HTTPS only (plain HTTP only to a server on the same machine); the token stays in the system's keyring. Shows, films and videos tied to a studyset follow the person in their settings as the server's and the item's ids alone (settings schema 9), named by the server when shown. The Watch screen plays the video (video_player on Android, media_kit's libmpv on Linux and Windows) with the line in the studyset's front language cut into the dictionary's words with furigana, and the back language's line beneath; a word tapped pauses the video and becomes a card with the line and its translation as its first hint, tagged with the show. The video's words are listed, most frequent first. The sound is the track in the studyset's language, whichever the file marks default, and the menu switches it; the subtitles read are a whole track, never a forced one (which only covers what the sound doesn't say), and captions for the hard of hearing only without plain ones. The line and its translation take the video's width in a box of a fixed height, so the video keeps its size; in the dark theme the video's colours glow around it, as YouTube's ambient mode, and the words' panel takes the colour of the episode's picture under the pointer. A line is cut into words with one request: DictionaryEndpoint.withKeys looks up every stretch of it that may be a word, as it stands and as every word it may be a conjugation of, in the new dictionary_key table (one row per key, indexed; migration dictionary-keys adds the table and its index, nothing else), which the loader fills beside the entries. The instance gets the text of each line looked up and keeps none of it, as the Watch screen says. Linux builds need libmpv-dev, and the app links libmpv: the snap stages Ubuntu's libmpv2, makes the update-alternatives links for BLAS and LAPACK that ffmpeg's speech recognition needs (staging never runs install scripts), and removes what the GNOME runtime and core24 already hold, as older copies of cairo, pango and librsvg hid the runtime's from its GTK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
61bf7b2fa0
commit
da51ef73cb
17 changed files with 25034 additions and 28 deletions
|
|
@ -92,6 +92,79 @@ class DictionaryEndpoint extends Endpoint {
|
|||
];
|
||||
}
|
||||
|
||||
/// The most keys [withKeys] takes at once.
|
||||
static const maxKeys = 4000;
|
||||
|
||||
/// The most entries [withKeys] gives for one key.
|
||||
static const maxPerKey = 8;
|
||||
|
||||
/// The entries of the dictionary of [source] with meanings in
|
||||
/// [glossLanguage] that have a key that is one of [keys] (at most
|
||||
/// [maxKeys]), each with that key; for each key its first [maxPerKey]
|
||||
/// entries in the dictionary's order. The app asks for every part of a
|
||||
/// sentence that may be a word (`食べられなかった`, and `食べる`, which
|
||||
/// that is a form of), to cut it into words with one request. Keys are
|
||||
/// compared as written, without the variants a search tries: they come
|
||||
/// from text, not from typing; a key with a space is never one.
|
||||
Future<List<DictionaryKeyHit>> withKeys(
|
||||
Session session,
|
||||
String source,
|
||||
String glossLanguage,
|
||||
List<String> keys,
|
||||
) async {
|
||||
if (keys.length > maxKeys) {
|
||||
throw ArgumentError.value(
|
||||
keys.length,
|
||||
'keys',
|
||||
'At most $maxKeys at once',
|
||||
);
|
||||
}
|
||||
final asked = {
|
||||
for (final key in keys)
|
||||
if (key.trim().isNotEmpty && !key.contains('|')) key.toLowerCase(),
|
||||
};
|
||||
if (asked.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
// SELECT "key", "sourceId" FROM "dictionary_key" WHERE the dictionary
|
||||
// is this one AND the key is one asked for; the first few of each key
|
||||
// by rank, in that order. Its index finds each key's rows.
|
||||
final rows = await session.db.unsafeQuery(
|
||||
'SELECT "key", "sourceId" FROM ('
|
||||
'SELECT "key", "sourceId", row_number() OVER '
|
||||
'(PARTITION BY "key" ORDER BY "rank", "sourceId") AS "place" '
|
||||
'FROM "dictionary_key" '
|
||||
'WHERE "source" = @source AND "glossLanguage" = @glossLanguage '
|
||||
'AND "key" = ANY(@keys)'
|
||||
') AS found WHERE "place" <= @perKey ORDER BY "key", "place"',
|
||||
parameters: QueryParameters.named({
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'keys': asked.toList(),
|
||||
'perKey': maxPerKey,
|
||||
}),
|
||||
);
|
||||
final found = [
|
||||
for (final row in rows)
|
||||
(key: row[0]! as String, sourceId: row[1]! as String),
|
||||
];
|
||||
final bySourceId = {
|
||||
for (final entry in await DictionaryEntry.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(source) &
|
||||
t.glossLanguage.equals(glossLanguage) &
|
||||
t.sourceId.inSet({for (final each in found) each.sourceId}),
|
||||
))
|
||||
entry.sourceId: entry,
|
||||
};
|
||||
return [
|
||||
for (final each in found)
|
||||
if (bySourceId[each.sourceId] case final entry?)
|
||||
DictionaryKeyHit(key: each.key, hit: _hit(entry)),
|
||||
];
|
||||
}
|
||||
|
||||
/// How many entries of the dictionary of [source] with meanings in
|
||||
/// [glossLanguage] carry [label].
|
||||
Future<int> countLabelled(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
### A key of a dictionary entry, one row each, for finding words as a text
|
||||
### writes them (`食べる`, `たべる`): `DictionaryEndpoint.withKeys` asks for
|
||||
### hundreds at once, which the entries' barred `searchKeys` could only
|
||||
### answer by reading the whole dictionary (1.2 s for JMdict); an index on
|
||||
### these answers at once. Written by the loader beside the entries, keys
|
||||
### with a space left out: text is looked up a word at a time.
|
||||
class: DictionaryKey
|
||||
table: dictionary_key
|
||||
fields:
|
||||
### The entry's source and meaning language, as on the entry.
|
||||
source: String
|
||||
glossLanguage: String
|
||||
### The key, in lower case, as the entry's `searchKeys` hold it.
|
||||
key: String
|
||||
### The entry's id in its source, which with the two above finds it.
|
||||
sourceId: String
|
||||
### The entry's rank, so the best entries of a key come first.
|
||||
rank: int
|
||||
indexes:
|
||||
# The entries with a key, in a dictionary.
|
||||
dictionary_key_idx:
|
||||
fields: source, glossLanguage, key
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
### A dictionary entry with one of its keys: what a text's words were found
|
||||
### by, the app looking up every way a sentence may be cut into words.
|
||||
class: DictionaryKeyHit
|
||||
fields:
|
||||
### The key asked for that the entry has (`食べる`).
|
||||
key: String
|
||||
hit: DictionaryHit
|
||||
|
|
@ -107,7 +107,14 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
where: (t) =>
|
||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||
);
|
||||
if (loaded?.fileHash == hash) {
|
||||
// A dictionary loaded before its keys had rows of their own loads again.
|
||||
if (loaded?.fileHash == hash &&
|
||||
await DictionaryKey.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||
) !=
|
||||
null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +132,15 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
await DictionaryKey.db.deleteWhere(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.source.equals(source) & t.glossLanguage.equals(glossLanguage),
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
final batch = <DictionaryEntry>[];
|
||||
final keys = <DictionaryKey>[];
|
||||
Future<void> flush() async {
|
||||
await DictionaryEntry.db.insert(
|
||||
session,
|
||||
|
|
@ -133,8 +148,15 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
await DictionaryKey.db.insert(
|
||||
session,
|
||||
keys,
|
||||
transaction: transaction,
|
||||
noReturn: true,
|
||||
);
|
||||
count += batch.length;
|
||||
batch.clear();
|
||||
keys.clear();
|
||||
}
|
||||
|
||||
await for (final line in _lines(file).skip(1)) {
|
||||
|
|
@ -145,17 +167,34 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
final headword = json['headword']! as String;
|
||||
final reading = json['reading'] as String?;
|
||||
final entry = jsonEncode(json['entry']);
|
||||
final sourceId = json['id']! as String;
|
||||
final rank = json['rank']! as int;
|
||||
final entryKeys = (json['keys']! as List).cast<String>();
|
||||
keys.addAll([
|
||||
for (final key in {
|
||||
for (final key in entryKeys)
|
||||
if (!key.contains(RegExp(r'\s')) && !key.contains('|'))
|
||||
key.toLowerCase(),
|
||||
})
|
||||
DictionaryKey(
|
||||
source: source,
|
||||
glossLanguage: glossLanguage,
|
||||
key: key,
|
||||
sourceId: sourceId,
|
||||
rank: rank,
|
||||
),
|
||||
]);
|
||||
batch.add(
|
||||
DictionaryEntry(
|
||||
source: source,
|
||||
sourceId: json['id']! as String,
|
||||
sourceId: sourceId,
|
||||
language: language,
|
||||
glossLanguage: glossLanguage,
|
||||
headword: headword,
|
||||
reading: reading,
|
||||
rank: json['rank']! as int,
|
||||
rank: rank,
|
||||
labels: barred((json['labels']! as List).cast<String>()),
|
||||
searchKeys: barred((json['keys']! as List).cast<String>()),
|
||||
searchKeys: barred(entryKeys),
|
||||
looseKeys: barred(
|
||||
(json['looseKeys'] as List? ?? const []).cast<String>(),
|
||||
),
|
||||
|
|
@ -202,6 +241,11 @@ Future<bool> loadDictionaryFile(Session session, File file) async {
|
|||
);
|
||||
}
|
||||
});
|
||||
// Fresh statistics, or Postgres plans withKeys' lookups as if the keys
|
||||
// were few and reads all of them (0.6 s for JMdict rather than 20 ms).
|
||||
await session.db.unsafeExecute(
|
||||
'ANALYZE "dictionary_entry", "dictionary_key"',
|
||||
);
|
||||
session.log(
|
||||
'Loaded the dictionary $name: $count entries of $source '
|
||||
'(${header['sourceVersion']}) in ${watch.elapsed.inSeconds} s.',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,688 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A key of a dictionary entry, one row each, for finding words as a text
|
||||
/// writes them (`食べる`, `たべる`): `DictionaryEndpoint.withKeys` asks for
|
||||
/// hundreds at once, which the entries' barred `searchKeys` could only
|
||||
/// answer by reading the whole dictionary (1.2 s for JMdict); an index on
|
||||
/// these answers at once. Written by the loader beside the entries, keys
|
||||
/// with a space left out: text is looked up a word at a time.
|
||||
abstract class DictionaryKey
|
||||
implements _is.TableRow<int?>, _is.ProtocolSerialization {
|
||||
DictionaryKey._({
|
||||
this.id,
|
||||
required this.source,
|
||||
required this.glossLanguage,
|
||||
required this.key,
|
||||
required this.sourceId,
|
||||
required this.rank,
|
||||
});
|
||||
|
||||
factory DictionaryKey({
|
||||
int? id,
|
||||
required String source,
|
||||
required String glossLanguage,
|
||||
required String key,
|
||||
required String sourceId,
|
||||
required int rank,
|
||||
}) = _DictionaryKeyImpl;
|
||||
|
||||
factory DictionaryKey.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return DictionaryKey(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
source: jsonSerialization['source'] as String,
|
||||
glossLanguage: jsonSerialization['glossLanguage'] as String,
|
||||
key: jsonSerialization['key'] as String,
|
||||
sourceId: jsonSerialization['sourceId'] as String,
|
||||
rank: jsonSerialization['rank'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
static final t = DictionaryKeyTable();
|
||||
|
||||
static const db = DictionaryKeyRepository._();
|
||||
|
||||
@override
|
||||
int? id;
|
||||
|
||||
/// The entry's source and meaning language, as on the entry.
|
||||
String source;
|
||||
|
||||
String glossLanguage;
|
||||
|
||||
/// The key, in lower case, as the entry's `searchKeys` hold it.
|
||||
String key;
|
||||
|
||||
/// The entry's id in its source, which with the two above finds it.
|
||||
String sourceId;
|
||||
|
||||
/// The entry's rank, so the best entries of a key come first.
|
||||
int rank;
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [DictionaryKey]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
DictionaryKey copyWith({
|
||||
int? id,
|
||||
String? source,
|
||||
String? glossLanguage,
|
||||
String? key,
|
||||
String? sourceId,
|
||||
int? rank,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'DictionaryKey',
|
||||
if (id != null) 'id': id,
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'key': key,
|
||||
'sourceId': sourceId,
|
||||
'rank': rank,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'DictionaryKey',
|
||||
if (id != null) 'id': id,
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'key': key,
|
||||
'sourceId': sourceId,
|
||||
'rank': rank,
|
||||
};
|
||||
}
|
||||
|
||||
static DictionaryKeyInclude include() {
|
||||
return DictionaryKeyInclude._();
|
||||
}
|
||||
|
||||
static DictionaryKeyIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<DictionaryKeyTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<DictionaryKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionaryKeyTable>? orderByList,
|
||||
DictionaryKeyInclude? include,
|
||||
}) {
|
||||
return DictionaryKeyIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(DictionaryKey.t),
|
||||
orderByList: orderByList?.call(DictionaryKey.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _DictionaryKeyImpl extends DictionaryKey {
|
||||
_DictionaryKeyImpl({
|
||||
int? id,
|
||||
required String source,
|
||||
required String glossLanguage,
|
||||
required String key,
|
||||
required String sourceId,
|
||||
required int rank,
|
||||
}) : super._(
|
||||
id: id,
|
||||
source: source,
|
||||
glossLanguage: glossLanguage,
|
||||
key: key,
|
||||
sourceId: sourceId,
|
||||
rank: rank,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [DictionaryKey]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
DictionaryKey copyWith({
|
||||
Object? id = _Undefined,
|
||||
String? source,
|
||||
String? glossLanguage,
|
||||
String? key,
|
||||
String? sourceId,
|
||||
int? rank,
|
||||
}) {
|
||||
return DictionaryKey(
|
||||
id: id is int? ? id : this.id,
|
||||
source: source ?? this.source,
|
||||
glossLanguage: glossLanguage ?? this.glossLanguage,
|
||||
key: key ?? this.key,
|
||||
sourceId: sourceId ?? this.sourceId,
|
||||
rank: rank ?? this.rank,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DictionaryKeyUpdateTable extends _is.UpdateTable<DictionaryKeyTable> {
|
||||
DictionaryKeyUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<String, String> source(String value) => _is.ColumnValue(
|
||||
table.source,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> glossLanguage(String value) =>
|
||||
_is.ColumnValue(
|
||||
table.glossLanguage,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> key(String value) => _is.ColumnValue(
|
||||
table.key,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> sourceId(String value) => _is.ColumnValue(
|
||||
table.sourceId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> rank(int value) => _is.ColumnValue(
|
||||
table.rank,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class DictionaryKeyTable extends _is.Table<int?> {
|
||||
DictionaryKeyTable({super.tableRelation})
|
||||
: super(tableName: 'dictionary_key') {
|
||||
updateTable = DictionaryKeyUpdateTable(this);
|
||||
source = _is.ColumnString(
|
||||
'source',
|
||||
this,
|
||||
);
|
||||
glossLanguage = _is.ColumnString(
|
||||
'glossLanguage',
|
||||
this,
|
||||
);
|
||||
key = _is.ColumnString(
|
||||
'key',
|
||||
this,
|
||||
);
|
||||
sourceId = _is.ColumnString(
|
||||
'sourceId',
|
||||
this,
|
||||
);
|
||||
rank = _is.ColumnInt(
|
||||
'rank',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final DictionaryKeyUpdateTable updateTable;
|
||||
|
||||
/// The entry's source and meaning language, as on the entry.
|
||||
late final _is.ColumnString source;
|
||||
|
||||
late final _is.ColumnString glossLanguage;
|
||||
|
||||
/// The key, in lower case, as the entry's `searchKeys` hold it.
|
||||
late final _is.ColumnString key;
|
||||
|
||||
/// The entry's id in its source, which with the two above finds it.
|
||||
late final _is.ColumnString sourceId;
|
||||
|
||||
/// The entry's rank, so the best entries of a key come first.
|
||||
late final _is.ColumnInt rank;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
source,
|
||||
glossLanguage,
|
||||
key,
|
||||
sourceId,
|
||||
rank,
|
||||
];
|
||||
}
|
||||
|
||||
class DictionaryKeyInclude extends _is.IncludeObject {
|
||||
DictionaryKeyInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => DictionaryKey.t;
|
||||
}
|
||||
|
||||
class DictionaryKeyIncludeList extends _is.IncludeList {
|
||||
DictionaryKeyIncludeList._({
|
||||
_is.WhereExpressionBuilder<DictionaryKeyTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(DictionaryKey.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => DictionaryKey.t;
|
||||
}
|
||||
|
||||
class DictionaryKeyRepository {
|
||||
const DictionaryKeyRepository._();
|
||||
|
||||
/// Returns a list of [DictionaryKey]s matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order of the items use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// The maximum number of items can be set by [limit]. If no limit is set,
|
||||
/// all items matching the query will be returned.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which [limit] (or all)
|
||||
/// items are read from the database.
|
||||
///
|
||||
/// ```dart
|
||||
/// var persons = await Persons.db.find(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.firstName,
|
||||
/// limit: 100,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<DictionaryKey>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<DictionaryKeyTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<DictionaryKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionaryKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<DictionaryKey>(
|
||||
where: where?.call(DictionaryKey.t),
|
||||
orderBy: orderBy?.call(DictionaryKey.t),
|
||||
orderByList: orderByList?.call(DictionaryKey.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [DictionaryKey] matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which the next one will be picked.
|
||||
///
|
||||
/// ```dart
|
||||
/// var youngestPerson = await Persons.db.findFirstRow(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.age,
|
||||
/// );
|
||||
/// ```
|
||||
Future<DictionaryKey?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<DictionaryKeyTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<DictionaryKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionaryKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<DictionaryKey>(
|
||||
where: where?.call(DictionaryKey.t),
|
||||
orderBy: orderBy?.call(DictionaryKey.t),
|
||||
orderByList: orderByList?.call(DictionaryKey.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [DictionaryKey] by its [id] or null if no such row exists.
|
||||
Future<DictionaryKey?> findById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<DictionaryKey>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [DictionaryKey]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [DictionaryKey]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// insert, none of the rows will be inserted.
|
||||
///
|
||||
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
|
||||
/// rows are silently skipped, and only the successfully inserted rows are
|
||||
/// returned.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the inserted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionaryKey>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<DictionaryKey> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<DictionaryKey>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [DictionaryKey] and returns the inserted row.
|
||||
///
|
||||
/// The returned [DictionaryKey] will have its `id` field set.
|
||||
Future<DictionaryKey> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
DictionaryKey row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<DictionaryKey>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [DictionaryKey]s in the list and returns the resulting rows.
|
||||
///
|
||||
/// If a row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated with the new values. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies to rows matching the
|
||||
/// given expression. Conflicting rows that don't match are skipped and not
|
||||
/// returned, so the resulting list may be shorter than [rows].
|
||||
///
|
||||
/// The returned [DictionaryKey]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails,
|
||||
/// none of the rows will be affected.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the resulting rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionaryKey>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<DictionaryKey> rows, {
|
||||
required _is.ColumnSelections<DictionaryKeyTable> conflictColumns,
|
||||
_is.ColumnSelections<DictionaryKeyTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<DictionaryKeyTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<DictionaryKey>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(DictionaryKey.t),
|
||||
updateColumns: updateColumns?.call(DictionaryKey.t),
|
||||
updateWhere: updateWhere?.call(DictionaryKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [DictionaryKey] and returns the resulting row.
|
||||
///
|
||||
/// If the row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies when the existing
|
||||
/// row matches the expression. Returns `null` if no row was affected — for
|
||||
/// example when [updateWhere] does not match the conflicting row.
|
||||
///
|
||||
/// The returned [DictionaryKey] will have its `id` field set.
|
||||
Future<DictionaryKey?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
DictionaryKey row, {
|
||||
required _is.ColumnSelections<DictionaryKeyTable> conflictColumns,
|
||||
_is.ColumnSelections<DictionaryKeyTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<DictionaryKeyTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<DictionaryKey>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(DictionaryKey.t),
|
||||
updateColumns: updateColumns?.call(DictionaryKey.t),
|
||||
updateWhere: updateWhere?.call(DictionaryKey.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [DictionaryKey]s in the list and returns the updated rows. If
|
||||
/// [columns] is provided, only those columns will be updated. Defaults to
|
||||
/// all columns.
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// update, none of the rows will be updated.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionaryKey>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<DictionaryKey> rows, {
|
||||
_is.ColumnSelections<DictionaryKeyTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<DictionaryKey>(
|
||||
rows,
|
||||
columns: columns?.call(DictionaryKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [DictionaryKey]. The row needs to have its id set.
|
||||
/// Optionally, a list of [columns] can be provided to only update those
|
||||
/// columns. Defaults to all columns.
|
||||
Future<DictionaryKey> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
DictionaryKey row, {
|
||||
_is.ColumnSelections<DictionaryKeyTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<DictionaryKey>(
|
||||
row,
|
||||
columns: columns?.call(DictionaryKey.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [DictionaryKey] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<DictionaryKey?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
required _is.ColumnValueListBuilder<DictionaryKeyUpdateTable> columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<DictionaryKey>(
|
||||
id,
|
||||
columnValues: columnValues(DictionaryKey.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [DictionaryKey]s matching the [where] expression with the specified [columnValues].
|
||||
/// Returns the list of updated rows.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionaryKey>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<DictionaryKeyUpdateTable> columnValues,
|
||||
required _is.WhereExpressionBuilder<DictionaryKeyTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<DictionaryKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionaryKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<DictionaryKey>(
|
||||
columnValues: columnValues(DictionaryKey.t.updateTable),
|
||||
where: where(DictionaryKey.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(DictionaryKey.t),
|
||||
orderByList: orderByList?.call(DictionaryKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [DictionaryKey]s in the list and returns the deleted rows.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fail to
|
||||
/// be deleted, none of the rows will be deleted.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionaryKey>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<DictionaryKey> rows, {
|
||||
_is.OrderByBuilder<DictionaryKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionaryKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<DictionaryKey>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(DictionaryKey.t),
|
||||
orderByList: orderByList?.call(DictionaryKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [DictionaryKey].
|
||||
Future<DictionaryKey> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
DictionaryKey row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<DictionaryKey>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all rows matching the [where] expression.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<DictionaryKey>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<DictionaryKeyTable> where,
|
||||
_is.OrderByBuilder<DictionaryKeyTable>? orderBy,
|
||||
_is.OrderByListBuilder<DictionaryKeyTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<DictionaryKey>(
|
||||
where: where(DictionaryKey.t),
|
||||
orderBy: orderBy?.call(DictionaryKey.t),
|
||||
orderByList: orderByList?.call(DictionaryKey.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<DictionaryKeyTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<DictionaryKey>(
|
||||
where: where?.call(DictionaryKey.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [DictionaryKey] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<DictionaryKeyTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<DictionaryKey>(
|
||||
where: where(DictionaryKey.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import '../dictionary/dictionary_hit.dart' as _in6hgebk;
|
||||
|
||||
/// A dictionary entry with one of its keys: what a text's words were found
|
||||
/// by, the app looking up every way a sentence may be cut into words.
|
||||
abstract class DictionaryKeyHit
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
DictionaryKeyHit._({
|
||||
required this.key,
|
||||
required this.hit,
|
||||
});
|
||||
|
||||
factory DictionaryKeyHit({
|
||||
required String key,
|
||||
required _in6hgebk.DictionaryHit hit,
|
||||
}) = _DictionaryKeyHitImpl;
|
||||
|
||||
factory DictionaryKeyHit.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return DictionaryKeyHit(
|
||||
key: jsonSerialization['key'] as String,
|
||||
hit: _i5lsenq5.Protocol().deserialize<_in6hgebk.DictionaryHit>(
|
||||
jsonSerialization['hit'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The key asked for that the entry has (`食べる`).
|
||||
String key;
|
||||
|
||||
_in6hgebk.DictionaryHit hit;
|
||||
|
||||
/// Returns a shallow copy of this [DictionaryKeyHit]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
DictionaryKeyHit copyWith({
|
||||
String? key,
|
||||
_in6hgebk.DictionaryHit? hit,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'DictionaryKeyHit',
|
||||
'key': key,
|
||||
'hit': hit.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'DictionaryKeyHit',
|
||||
'key': key,
|
||||
'hit': hit.toJsonForProtocol(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _DictionaryKeyHitImpl extends DictionaryKeyHit {
|
||||
_DictionaryKeyHitImpl({
|
||||
required String key,
|
||||
required _in6hgebk.DictionaryHit hit,
|
||||
}) : super._(
|
||||
key: key,
|
||||
hit: hit,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [DictionaryKeyHit]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
DictionaryKeyHit copyWith({
|
||||
String? key,
|
||||
_in6hgebk.DictionaryHit? hit,
|
||||
}) {
|
||||
return DictionaryKeyHit(
|
||||
key: key ?? this.key,
|
||||
hit: hit ?? this.hit.copyWith(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -464,6 +464,38 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
limit: params['limit'],
|
||||
),
|
||||
),
|
||||
'withKeys': _is.MethodConnector(
|
||||
name: 'withKeys',
|
||||
params: {
|
||||
'source': _is.ParameterDescription(
|
||||
name: 'source',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'glossLanguage': _is.ParameterDescription(
|
||||
name: 'glossLanguage',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'keys': _is.ParameterDescription(
|
||||
name: 'keys',
|
||||
type: _is.getType<List<String>>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['dictionary'] as _ilgtwlto.DictionaryEndpoint)
|
||||
.withKeys(
|
||||
session,
|
||||
params['source'],
|
||||
params['glossLanguage'],
|
||||
params['keys'],
|
||||
),
|
||||
),
|
||||
'countLabelled': _is.MethodConnector(
|
||||
name: 'countLabelled',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import 'package:halkyon_learn_server/src/generated/auth/device.dart'
|
|||
as _ipdmo8cq;
|
||||
import 'package:halkyon_learn_server/src/generated/dictionary/dictionary_hit.dart'
|
||||
as _ism30i9n;
|
||||
import 'package:halkyon_learn_server/src/generated/dictionary/dictionary_key_hit.dart'
|
||||
as _iymncpv9;
|
||||
import 'package:halkyon_learn_server/src/generated/dictionary/dictionary_source.dart'
|
||||
as _i4at7f2s;
|
||||
import 'package:halkyon_learn_server/src/generated/explore/explore_entry.dart'
|
||||
|
|
@ -51,6 +53,8 @@ import 'common/not_found_exception.dart' as _i8oee73b;
|
|||
import 'dictionary/dictionary_credit.dart' as _ii4hopsr;
|
||||
import 'dictionary/dictionary_entry.dart' as _i4ipxv77;
|
||||
import 'dictionary/dictionary_hit.dart' as _ivbh47kc;
|
||||
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
|
||||
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
|
||||
import 'dictionary/dictionary_source.dart' as _ie12eb65;
|
||||
import 'explore/explore_entry.dart' as _iaxdli27;
|
||||
import 'explore/explore_query.dart' as _ibl9h9o9;
|
||||
|
|
@ -110,6 +114,8 @@ export 'common/not_found_exception.dart';
|
|||
export 'dictionary/dictionary_credit.dart';
|
||||
export 'dictionary/dictionary_entry.dart';
|
||||
export 'dictionary/dictionary_hit.dart';
|
||||
export 'dictionary/dictionary_key.dart';
|
||||
export 'dictionary/dictionary_key_hit.dart';
|
||||
export 'dictionary/dictionary_source.dart';
|
||||
export 'explore/explore_entry.dart';
|
||||
export 'explore/explore_query.dart';
|
||||
|
|
@ -1128,6 +1134,76 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'dictionary_key',
|
||||
dartName: 'DictionaryKey',
|
||||
schema: 'public',
|
||||
module: 'halkyon_learn',
|
||||
columns: [
|
||||
_isp.ColumnDefinition(
|
||||
name: 'id',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int?',
|
||||
columnDefault: 'serial',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'source',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'glossLanguage',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'key',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'sourceId',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'rank',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
],
|
||||
foreignKeys: [],
|
||||
indexes: [
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'dictionary_key_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'source',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'glossLanguage',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'key',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'dictionary_source',
|
||||
dartName: 'DictionarySource',
|
||||
|
|
@ -3533,6 +3609,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _ivbh47kc.DictionaryHit) {
|
||||
return _ivbh47kc.DictionaryHit.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i9gd3xbn.DictionaryKey) {
|
||||
return _i9gd3xbn.DictionaryKey.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i6b9unou.DictionaryKeyHit) {
|
||||
return _i6b9unou.DictionaryKeyHit.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ie12eb65.DictionarySource) {
|
||||
return _ie12eb65.DictionarySource.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -3737,6 +3819,14 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return (data != null ? _ivbh47kc.DictionaryHit.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_i9gd3xbn.DictionaryKey?>()) {
|
||||
return (data != null ? _i9gd3xbn.DictionaryKey.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_i6b9unou.DictionaryKeyHit?>()) {
|
||||
return (data != null ? _i6b9unou.DictionaryKeyHit.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_ie12eb65.DictionarySource?>()) {
|
||||
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
|
||||
as T;
|
||||
|
|
@ -3937,15 +4027,21 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_iymncpv9.DictionaryKeyHit>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_iymncpv9.DictionaryKeyHit>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<String>) {
|
||||
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
|
||||
}
|
||||
if (t == Map<String, String>) {
|
||||
return (data as Map).map(
|
||||
(k, v) => MapEntry(deserialize<String>(k), deserialize<String>(v)),
|
||||
)
|
||||
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))
|
||||
|
|
@ -3999,6 +4095,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_ii4hopsr.DictionaryCredit => 'DictionaryCredit',
|
||||
_i4ipxv77.DictionaryEntry => 'DictionaryEntry',
|
||||
_ivbh47kc.DictionaryHit => 'DictionaryHit',
|
||||
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
|
||||
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
|
||||
_ie12eb65.DictionarySource => 'DictionarySource',
|
||||
_iaxdli27.ExploreEntry => 'ExploreEntry',
|
||||
_ibl9h9o9.ExploreQuery => 'ExploreQuery',
|
||||
|
|
@ -4095,6 +4193,10 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'DictionaryEntry';
|
||||
case _ivbh47kc.DictionaryHit():
|
||||
return 'DictionaryHit';
|
||||
case _i9gd3xbn.DictionaryKey():
|
||||
return 'DictionaryKey';
|
||||
case _i6b9unou.DictionaryKeyHit():
|
||||
return 'DictionaryKeyHit';
|
||||
case _ie12eb65.DictionarySource():
|
||||
return 'DictionarySource';
|
||||
case _iaxdli27.ExploreEntry():
|
||||
|
|
@ -4265,6 +4367,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'DictionaryHit') {
|
||||
return deserialize<_ivbh47kc.DictionaryHit>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionaryKey') {
|
||||
return deserialize<_i9gd3xbn.DictionaryKey>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionaryKeyHit') {
|
||||
return deserialize<_i6b9unou.DictionaryKeyHit>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'DictionarySource') {
|
||||
return deserialize<_ie12eb65.DictionarySource>(data['data']);
|
||||
}
|
||||
|
|
@ -4454,6 +4562,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return _ipbyuef1.CardRevision.t;
|
||||
case _i4ipxv77.DictionaryEntry:
|
||||
return _i4ipxv77.DictionaryEntry.t;
|
||||
case _i9gd3xbn.DictionaryKey:
|
||||
return _i9gd3xbn.DictionaryKey.t;
|
||||
case _ie12eb65.DictionarySource:
|
||||
return _ie12eb65.DictionarySource.t;
|
||||
case _is663hjt.FeedbackBlock:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ gitHubUpgrade:
|
|||
dictionary:
|
||||
- dictionaries:
|
||||
- search:
|
||||
- withKeys:
|
||||
- countLabelled:
|
||||
- labelled:
|
||||
- changed:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "dictionary_key",
|
||||
"dartName": "DictionaryKey",
|
||||
"module": "halkyon_learn",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"columnDefault": "serial",
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "source",
|
||||
"fieldName": "source",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "glossLanguage",
|
||||
"fieldName": "glossLanguage",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "key",
|
||||
"fieldName": "key",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "sourceId",
|
||||
"fieldName": "sourceId",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "rank",
|
||||
"fieldName": "rank",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "dictionary_key_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "source"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "glossLanguage"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "key"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
BEGIN;
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "dictionary_key" (
|
||||
"id" bigserial PRIMARY KEY,
|
||||
"source" text NOT NULL,
|
||||
"glossLanguage" text NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"sourceId" text NOT NULL,
|
||||
"rank" bigint NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "dictionary_key_idx" ON "dictionary_key" USING btree ("source", "glossLanguage", "key");
|
||||
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_learn
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_learn', '20261013035552510-dictionary-keys', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261013035552510-dictionary-keys', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod', '20260824182259319', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_idp
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_idp', '20260924105404509', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_offline_sync
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_offline_sync', '20260914143806119', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260914143806119', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_core
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_core', '20260924105232991', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
|
||||
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -26,3 +26,4 @@
|
|||
20261009224606274-feedback
|
||||
20261010095547719-assistant-connector
|
||||
20261012162411835-studyset-deleted-at
|
||||
20261013035552510-dictionary-keys
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ void main() {
|
|||
|
||||
tearDown(() async {
|
||||
await DictionaryEntry.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await DictionaryKey.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await DictionarySource.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id > 0,
|
||||
|
|
@ -620,6 +621,114 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('withKeys: the entries with a key that is one asked for, each '
|
||||
'with that key; a key only starting one finds nothing, nor does '
|
||||
"another dictionary's entry", () async {
|
||||
await loadDictionaryFile(
|
||||
session,
|
||||
write([taberu, terebi, toukyou, kuidaore]),
|
||||
);
|
||||
|
||||
Future<List<(String, String)>> withKeys(
|
||||
List<String> keys, {
|
||||
String glossLanguage = 'en',
|
||||
}) async => [
|
||||
for (final found in await endpoints.dictionary.withKeys(
|
||||
signedIn,
|
||||
'jmdict',
|
||||
glossLanguage,
|
||||
keys,
|
||||
))
|
||||
(found.key, found.hit.headword),
|
||||
];
|
||||
|
||||
expect(
|
||||
(await withKeys([
|
||||
'食べる',
|
||||
'たべ',
|
||||
'テレビ',
|
||||
'てれび',
|
||||
'東京',
|
||||
'EAT',
|
||||
'nothing',
|
||||
'',
|
||||
'a|b',
|
||||
])).toSet(),
|
||||
{
|
||||
('食べる', '食べる'),
|
||||
('テレビ', 'テレビ'),
|
||||
('てれび', 'テレビ'),
|
||||
('東京', '東京'),
|
||||
('eat', '食べる'),
|
||||
},
|
||||
);
|
||||
expect(await withKeys(['食べる'], glossLanguage: 'de'), isEmpty);
|
||||
expect(await withKeys([]), isEmpty);
|
||||
expect(
|
||||
() => withKeys([
|
||||
for (var i = 0; i <= DictionaryEndpoint.maxKeys; i++) '$i',
|
||||
]),
|
||||
throwsA(anything),
|
||||
);
|
||||
});
|
||||
|
||||
test('withKeys finds the keys loaded with their entries, and no more '
|
||||
'once a load replaced them; a dictionary loaded before keys had '
|
||||
'rows loads again', () async {
|
||||
final file = write([taberu, terebi]);
|
||||
await loadDictionaryFile(session, file);
|
||||
expect(
|
||||
await DictionaryKey.db.count(session),
|
||||
// 食べる, たべる, taberu, eat; テレビ, てれび, terebi, television,
|
||||
// tv: "to eat" has a space.
|
||||
9,
|
||||
);
|
||||
|
||||
write([terebi], head: header(version: '3.6.3 (2026-11-01)'));
|
||||
await loadDictionaryFile(session, file);
|
||||
expect(
|
||||
await endpoints.dictionary.withKeys(signedIn, 'jmdict', 'en', [
|
||||
'食べる',
|
||||
]),
|
||||
isEmpty,
|
||||
);
|
||||
|
||||
await DictionaryKey.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
expect(await loadDictionaryFile(session, file), isTrue);
|
||||
expect(await DictionaryKey.db.count(session), 5);
|
||||
expect(await loadDictionaryFile(session, file), isFalse);
|
||||
});
|
||||
|
||||
test('withKeys: for a key many entries have, the first few in the '
|
||||
"dictionary's order", () async {
|
||||
await loadDictionaryFile(
|
||||
session,
|
||||
write([
|
||||
for (var i = 0; i < DictionaryEndpoint.maxPerKey + 2; i++)
|
||||
entry(
|
||||
'${2000 + i}',
|
||||
'はし$i',
|
||||
rank: 100 - i,
|
||||
keys: ['はし$i', 'はし', 'hashi'],
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
final found = await endpoints.dictionary.withKeys(
|
||||
signedIn,
|
||||
'jmdict',
|
||||
'en',
|
||||
['はし'],
|
||||
);
|
||||
|
||||
expect(found, hasLength(DictionaryEndpoint.maxPerKey));
|
||||
expect(
|
||||
found.first.hit.headword,
|
||||
'はし${DictionaryEndpoint.maxPerKey + 1}',
|
||||
);
|
||||
expect(found.map((each) => each.key).toSet(), {'はし'});
|
||||
});
|
||||
|
||||
test('the dictionaries are for those signed in', () {
|
||||
expect(
|
||||
() => endpoints.dictionary.search(
|
||||
|
|
@ -630,6 +739,12 @@ void main() {
|
|||
),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
);
|
||||
expect(
|
||||
() => endpoints.dictionary.withKeys(sessionBuilder, 'jmdict', 'en', [
|
||||
'食べる',
|
||||
]),
|
||||
throwsA(isA<ServerpodUnauthenticatedException>()),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import 'package:halkyon_learn_server/src/generated/auth/device.dart'
|
|||
as _ipdmo8cq;
|
||||
import 'package:halkyon_learn_server/src/generated/dictionary/dictionary_hit.dart'
|
||||
as _ism30i9n;
|
||||
import 'package:halkyon_learn_server/src/generated/dictionary/dictionary_key_hit.dart'
|
||||
as _iymncpv9;
|
||||
import 'package:halkyon_learn_server/src/generated/dictionary/dictionary_source.dart'
|
||||
as _i4at7f2s;
|
||||
import 'package:halkyon_learn_server/src/generated/explore/explore_entry.dart'
|
||||
|
|
@ -862,6 +864,43 @@ class _DictionaryEndpoint {
|
|||
});
|
||||
}
|
||||
|
||||
_ida.Future<List<_iymncpv9.DictionaryKeyHit>> withKeys(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
String source,
|
||||
String glossLanguage,
|
||||
List<String> keys,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'dictionary',
|
||||
method: 'withKeys',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'dictionary',
|
||||
methodName: 'withKeys',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'source': source,
|
||||
'glossLanguage': glossLanguage,
|
||||
'keys': keys,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<List<_iymncpv9.DictionaryKeyHit>>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<int> countLabelled(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
String source,
|
||||
|
|
|
|||
Loading…
Reference in a new issue