feat(dictionary): Japanese cut into words and looked up on the device
Once the person agrees, the device holds MeCab's IPADIC and the
instance's JMdict and cuts lines as Jisho.org does, so their text never
leaves it (ADR 0012). MeCab comes through mecab_for_dart, whose build
hook compiles it; its tokens are grouped by Ve's rules, written anew in
Dart, with に kept a word of its own. Each word is found as it stands or
as a form of a word, else by its dictionary form, else as the longest
words it holds from token to token; longer words over token boundaries
are offered as before.
The curator packs Ubuntu's compiled IPADIC beside the dictionary files
(curate ipadic, and every update run). The server exports its JMdict
from its tables, so the device finds the same keys and keeps the same
versions, and serves both at /device-dictionaries/, named in
dictionary.deviceFiles('ja') with their hashes; Caddy forwards the path.
The app checks each file against its hash, unpacks IPADIC, builds a
SQLite database from the export on an isolate, and asks the instance
for newer files every 30 days, downloading only what changed. The Watch
screen offers the download once per device with its size (34 MB, some
150 MB once set up); Settings downloads and removes them. Without them,
or should cutting on the device fail, lines go to the instance as
before. MeCab's, IPADIC's and Ve's licences join the licence list.
Every build now needs a C++ compiler; mecab_for_dart holds code_assets
at 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8fe2cf3436
commit
1d2011a676
17 changed files with 5487 additions and 245 deletions
|
|
@ -39,6 +39,7 @@ import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
|||
import '../../support/fake_asset_gateway.dart';
|
||||
import '../../support/fake_asset_server_gateway.dart';
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_learning_path_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
|
|
@ -86,7 +87,7 @@ final class _Person() {
|
|||
|
||||
ArchiveImporter importer() => ArchiveImporter(
|
||||
ReleaseCopier(studysets, cards, notes, paths, pdfs, tags, assets),
|
||||
DictionaryRepository(FakeDictionaryGateway()),
|
||||
DictionaryRepository(FakeDictionaryGateway(), noDeviceDictionaries()),
|
||||
reviews,
|
||||
progress,
|
||||
studysets,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
|||
import '../../support/fake_asset_gateway.dart';
|
||||
import '../../support/fake_asset_server_gateway.dart';
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_learning_path_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
|
|
@ -291,6 +292,7 @@ void main() {
|
|||
),
|
||||
],
|
||||
),
|
||||
noDeviceDictionaries(),
|
||||
),
|
||||
reviews,
|
||||
PathProgressRepository(FakePathAnswerGateway()),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/analysed_words.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/mecab_tokens.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/sentence_words.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/ve_words.dart';
|
||||
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/mecab_output.dart';
|
||||
|
||||
final words = [
|
||||
word('1', '今日', reading: 'きょう', pos: ['n', 'adv'], labels: ['common']),
|
||||
word('2', '今日は', reading: 'こんにちは', pos: ['int'], labels: ['common']),
|
||||
word('3', 'は', pos: ['prt'], labels: ['common']),
|
||||
word('4', 'いい', pos: ['adj-ix'], labels: ['common']),
|
||||
word('5', '天気', reading: 'てんき', pos: ['n'], labels: ['common']),
|
||||
word('6', 'です', pos: ['cop'], labels: ['common']),
|
||||
word('7', 'ね', pos: ['prt'], labels: ['common']),
|
||||
word('8', '食べる', reading: 'たべる', pos: ['v1', 'vt']),
|
||||
word('9', '静か', reading: 'しずか', pos: ['adj-na']),
|
||||
word('10', '部屋', reading: 'へや', pos: ['n']),
|
||||
word('11', '二十', reading: 'にじゅう', pos: ['num']),
|
||||
word('12', '五', reading: 'ご', pos: ['num']),
|
||||
word('13', '歳', reading: 'さい', pos: ['ctr']),
|
||||
word('14', '携帯', reading: 'けいたい', pos: ['n', 'vs']),
|
||||
word('15', '電話', reading: 'でんわ', pos: ['n', 'vs']),
|
||||
word('16', '携帯電話', reading: 'けいたいでんわ', pos: ['n']),
|
||||
word('17', 'を', pos: ['prt']),
|
||||
word('18', '買う', reading: 'かう', pos: ['v5u', 'vt']),
|
||||
word('19', 'だ', pos: ['cop']),
|
||||
word('20', 'よ', pos: ['prt']),
|
||||
word('21', 'さん', pos: ['suf']),
|
||||
word('22', 'が', pos: ['prt'], labels: ['common']),
|
||||
word('23', '来る', reading: 'くる', pos: ['vk']),
|
||||
word('24', 'オーケー', reading: 'ok', pos: ['int']),
|
||||
word('25', '𠮟る', reading: 'しかる', pos: ['v5r', 'vt']),
|
||||
word('26', '勉強', reading: 'べんきょう', pos: ['n', 'vs']),
|
||||
word('27', '寝る', reading: 'ねる', pos: ['v1', 'vi']),
|
||||
word('28', 'おはよう', pos: ['int']),
|
||||
word('29', 'と', pos: ['prt']),
|
||||
word('30', '言う', reading: 'いう', pos: ['v5u', 'vt']),
|
||||
];
|
||||
|
||||
/// [sentence] cut into words on the device, with the dictionary [words]
|
||||
/// as the fake gateway finds them by their keys.
|
||||
Future<List<SentencePart>> partsOf(String sentence) async {
|
||||
final grouped = veWords(mecabTokens(sentence, mecabOutput[sentence]!));
|
||||
final keys = analysedSentenceKeys(sentence, grouped);
|
||||
final found = await FakeDictionaryGateway(
|
||||
words: words,
|
||||
).withKeys(jmdict, keys.toList());
|
||||
return analysedSentenceParts(sentence, grouped, found);
|
||||
}
|
||||
|
||||
List<String> textsOf(List<SentencePart> parts) => [
|
||||
for (final part in parts)
|
||||
if (part.isWord) part.text else '(${part.text})',
|
||||
];
|
||||
|
||||
void main() {
|
||||
test('knows grammar the longest match doesn’t: 今日は is 今日 and は where '
|
||||
'the sentence goes on', () async {
|
||||
final parts = await partsOf('今日はいい天気ですね');
|
||||
|
||||
expect(textsOf(parts), ['今日', 'は', 'いい', '天気', 'です', 'ね']);
|
||||
expect(parts.first.entries.first.headword, '今日');
|
||||
expect(parts[1].entries.first.headword, 'は');
|
||||
expect(
|
||||
parts.first.wider.single.entries.single.reading,
|
||||
'こんにちは',
|
||||
reason: 'the longer word is still offered',
|
||||
);
|
||||
});
|
||||
|
||||
test('finds a conjugated word by its dictionary form, with how it is '
|
||||
'conjugated', () async {
|
||||
final [eat] = await partsOf('食べられなかった');
|
||||
|
||||
expect(eat.text, '食べられなかった');
|
||||
expect(eat.entries.single.headword, '食べる');
|
||||
expect(eat.steps, isNotEmpty);
|
||||
});
|
||||
|
||||
test('finds a word Ve joined with its ending by its dictionary form, '
|
||||
'where the dictionary lacks it as it stands', () async {
|
||||
final [quiet, room] = await partsOf('静かな部屋');
|
||||
|
||||
expect(quiet.text, '静かな');
|
||||
expect(quiet.entries.single.headword, '静か');
|
||||
expect(room.entries.single.headword, '部屋');
|
||||
|
||||
final [study] = await partsOf('勉強しなければならない');
|
||||
expect(study.text, '勉強しなければならない');
|
||||
expect(study.entries.single.headword, '勉強');
|
||||
});
|
||||
|
||||
test('cuts a group the dictionary lacks into the longest words it holds, '
|
||||
'from token to token', () async {
|
||||
final parts = await partsOf('二十五歳です');
|
||||
|
||||
expect(textsOf(parts), ['二十', '五', '歳', 'です']);
|
||||
});
|
||||
|
||||
test(
|
||||
'offers the longer words running over a word into its neighbours',
|
||||
() async {
|
||||
final [mobile, phone, _, buy] = await partsOf('携帯電話を買った');
|
||||
|
||||
expect(mobile.text, '携帯');
|
||||
expect(mobile.wider.single.text, '携帯電話');
|
||||
expect(phone.wider.single.text, '携帯電話');
|
||||
expect(phone.wider.single.start, 0);
|
||||
expect(buy.entries.single.headword, '買う');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'leaves what is no word as text, never looking up romaji or meanings',
|
||||
() async {
|
||||
const sentence = 'OKだよ';
|
||||
final grouped = veWords(mecabTokens(sentence, mecabOutput[sentence]!));
|
||||
|
||||
expect(analysedSentenceKeys(sentence, grouped), isNot(contains('OK')));
|
||||
expect(analysedSentenceKeys(sentence, grouped), isNot(contains('ok')));
|
||||
expect(textsOf(await partsOf(sentence)), ['(OK)', 'だ', 'よ']);
|
||||
expect(textsOf(await partsOf('「おはよう」と言った')), [
|
||||
'(「)',
|
||||
'おはよう',
|
||||
'(」)',
|
||||
'と',
|
||||
'言った',
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test('keeps a name the dictionary lacks as text, and the suffix after it '
|
||||
'a word', () async {
|
||||
final parts = await partsOf('田中さんが来た');
|
||||
|
||||
expect(textsOf(parts), ['(田中)', 'さん', 'が', '来た']);
|
||||
expect(parts.last.entries.single.headword, '来る');
|
||||
});
|
||||
|
||||
test('places each part where it stands, characters beyond the basic plane '
|
||||
'counted as Dart counts them', () async {
|
||||
final [scold] = await partsOf('𠮟られた');
|
||||
|
||||
expect(scold.text, '𠮟られた');
|
||||
expect(scold.start, 0);
|
||||
expect(scold.entries.single.headword, '𠮟る');
|
||||
|
||||
final parts = await partsOf('食べて 寝た');
|
||||
expect(
|
||||
[for (final part in parts) (part.text, part.start)],
|
||||
[
|
||||
('食べて', 0),
|
||||
(' ', 3),
|
||||
('寝た', 4),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('asks for each stretch from token to token and each dictionary form, '
|
||||
'with the words they may be forms of', () {
|
||||
const sentence = '静かな部屋';
|
||||
final keys = analysedSentenceKeys(
|
||||
sentence,
|
||||
veWords(mecabTokens(sentence, mecabOutput[sentence]!)),
|
||||
);
|
||||
|
||||
expect(keys, containsAll(['静か', '静かな', '静かな部屋', '部屋', 'な', 'な部屋']));
|
||||
expect(keys, isNot(contains('かな')), reason: 'no token starts there');
|
||||
});
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/pinyin_ruby.d
|
|||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/text_words.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/word_levels.dart';
|
||||
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
|
||||
/// CC-CEDICT in simplified characters, as an instance that loaded it lists
|
||||
|
|
@ -55,6 +56,7 @@ void main() {
|
|||
available: const [jmdict, cedictHans],
|
||||
words: chineseWords,
|
||||
),
|
||||
noDeviceDictionaries(),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,332 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/device_dictionaries.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/device_dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/sentence_words.dart';
|
||||
|
||||
import '../../support/device_export.dart';
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
|
||||
/// The curator's IPADIC pack, holding [names] (each its name as content).
|
||||
List<int> ipadicPack({
|
||||
Iterable<String> names = const [
|
||||
'char.bin',
|
||||
'dicrc',
|
||||
'matrix.bin',
|
||||
'sys.dic',
|
||||
'unk.dic',
|
||||
'mecabrc',
|
||||
'COPYING',
|
||||
],
|
||||
}) {
|
||||
final archive = Archive();
|
||||
for (final name in names) {
|
||||
archive.add(
|
||||
ArchiveFile.bytes(
|
||||
name,
|
||||
Uint8List.fromList(utf8.encode('$name of IPADIC')),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const GZipEncoder().encodeBytes(TarEncoder().encodeBytes(archive));
|
||||
}
|
||||
|
||||
/// [bytes] offered as the file [name] at [url].
|
||||
DeviceFile offered(String name, String url, List<int> bytes) => DeviceFile(
|
||||
name: name,
|
||||
url: Uri.parse(url),
|
||||
sha256: '${sha256.convert(bytes)}',
|
||||
size: bytes.length,
|
||||
);
|
||||
|
||||
void main() {
|
||||
late Directory folder;
|
||||
late List<int> pack;
|
||||
late List<int> export;
|
||||
late FakeDeviceDictionaryGateway gateway;
|
||||
late FakeJapaneseAnalyser analyser;
|
||||
|
||||
final ipadicUrl = Uri.parse(
|
||||
'https://learn.example.org/device-dictionaries/i',
|
||||
);
|
||||
final exportUrl = Uri.parse(
|
||||
'https://learn.example.org/device-dictionaries/j',
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
folder = Directory.systemTemp.createTempSync('dictionaries');
|
||||
pack = ipadicPack();
|
||||
export = exportBytes([
|
||||
exportEntry('1358280', '食べる', reading: 'たべる'),
|
||||
]);
|
||||
gateway = FakeDeviceDictionaryGateway(
|
||||
offered: [
|
||||
offered('mecab-ipadic', '$ipadicUrl', pack),
|
||||
offered('jmdict-en', '$exportUrl', export),
|
||||
],
|
||||
bytes: {ipadicUrl: pack, exportUrl: export},
|
||||
);
|
||||
analyser = FakeJapaneseAnalyser(
|
||||
cut: (sentence) => [SentencePart(text: '[$sentence]', start: 0)],
|
||||
);
|
||||
});
|
||||
tearDown(() => folder.deleteSync(recursive: true));
|
||||
|
||||
DeviceDictionaries open() =>
|
||||
DeviceDictionaries.open(gateway, analyser, folder: folder);
|
||||
|
||||
/// The names in [folder], but the manifest.
|
||||
Set<String> held() => {
|
||||
for (final entry in folder.listSync())
|
||||
if (!entry.path.endsWith('manifest.json'))
|
||||
entry.uri.pathSegments.lastWhere((segment) => segment.isNotEmpty),
|
||||
};
|
||||
|
||||
test('lists what the instance offers devices; none when it offers none '
|
||||
'or can’t be reached', () async {
|
||||
expect(await open().offered(), gateway.offered);
|
||||
|
||||
gateway.offered = const [];
|
||||
expect(await open().offered(), isEmpty);
|
||||
|
||||
gateway
|
||||
..offered = [offered('jmdict-en', '$exportUrl', export)]
|
||||
..fail = const SocketException('offline');
|
||||
expect(await open().offered(), isEmpty);
|
||||
});
|
||||
|
||||
test('downloads and installs them: IPADIC unpacked, the dictionary built, '
|
||||
'the downloads gone, all kept for the next start', () async {
|
||||
final dictionaries = open();
|
||||
|
||||
await dictionaries.download();
|
||||
|
||||
final installed = dictionaries.status.installed!;
|
||||
expect(installed.source, 'jmdict');
|
||||
expect(installed.glossLanguage, 'en');
|
||||
expect(installed.files, {
|
||||
'mecab-ipadic': '${sha256.convert(pack)}',
|
||||
'jmdict-en': '${sha256.convert(export)}',
|
||||
});
|
||||
expect(installed.bytes, greaterThan(0));
|
||||
expect(held(), {installed.ipadic, installed.database});
|
||||
expect(
|
||||
File('${folder.path}/${installed.ipadic}/sys.dic').readAsStringSync(),
|
||||
'sys.dic of IPADIC',
|
||||
);
|
||||
expect(dictionaries.covers(jmdict), isTrue);
|
||||
expect(dictionaries.covers(kanjidic), isFalse);
|
||||
expect(dictionaries.covers(ccCedictHans), isFalse);
|
||||
expect(open().status.installed, installed);
|
||||
});
|
||||
|
||||
test(
|
||||
'shows how far a download has come, then that it prepares them',
|
||||
() async {
|
||||
final dictionaries = open();
|
||||
final states = <DeviceDictionaryStatus>[];
|
||||
dictionaries.changes.listen(states.add);
|
||||
|
||||
await dictionaries.download();
|
||||
await pumpEventQueue();
|
||||
|
||||
final progress = [for (final state in states) ?state.progress];
|
||||
expect(progress.first.received, 0);
|
||||
expect(
|
||||
progress.where((step) => step.received > 0 && !step.preparing),
|
||||
isNotEmpty,
|
||||
);
|
||||
expect(progress.last.preparing, isTrue);
|
||||
expect(progress.last.total, pack.length + export.length);
|
||||
expect(states.last.progress, isNull);
|
||||
expect(states.last.installed, isNotNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('refuses a file that isn’t the one the instance named, keeps nothing '
|
||||
'of it, and says so', () async {
|
||||
gateway.bytes[exportUrl] = [...export, 0];
|
||||
final dictionaries = open();
|
||||
|
||||
await dictionaries.download();
|
||||
|
||||
expect(dictionaries.status.installed, isNull);
|
||||
expect(dictionaries.status.failed, isTrue);
|
||||
expect(dictionaries.status.progress, isNull);
|
||||
expect(held(), isEmpty);
|
||||
expect(dictionaries.covers(jmdict), isFalse);
|
||||
});
|
||||
|
||||
test('refuses an IPADIC missing a file MeCab reads, and unpacks nothing but '
|
||||
'what MeCab reads', () {
|
||||
final bad = ipadicPack(names: ['char.bin', 'dicrc', 'matrix.bin']);
|
||||
final into = Directory('${folder.path}/ipadic');
|
||||
File('${folder.path}/bad.tar.gz').writeAsBytesSync(bad);
|
||||
expect(
|
||||
() => unpackIpadic(File('${folder.path}/bad.tar.gz'), into),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(into.existsSync(), isFalse);
|
||||
|
||||
File('${folder.path}/odd.tar.gz').writeAsBytesSync(
|
||||
ipadicPack(
|
||||
names: [
|
||||
'char.bin',
|
||||
'dicrc',
|
||||
'matrix.bin',
|
||||
'sys.dic',
|
||||
'unk.dic',
|
||||
'mecabrc',
|
||||
'../escaped',
|
||||
'extra.txt',
|
||||
],
|
||||
),
|
||||
);
|
||||
unpackIpadic(File('${folder.path}/odd.tar.gz'), into);
|
||||
expect(
|
||||
{for (final file in into.listSync()) file.uri.pathSegments.last},
|
||||
{'char.bin', 'dicrc', 'matrix.bin', 'sys.dic', 'unk.dic', 'mecabrc'},
|
||||
);
|
||||
expect(File('${folder.path}/escaped').existsSync(), isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'asks once per device: the answer is kept, and agreeing downloads',
|
||||
() async {
|
||||
final declined = open();
|
||||
expect(declined.status.asked, isFalse);
|
||||
|
||||
await declined.answer(agreed: false);
|
||||
|
||||
expect(declined.status.asked, isTrue);
|
||||
expect(gateway.downloaded, isEmpty);
|
||||
expect(open().status.asked, isTrue);
|
||||
|
||||
final agreed = open();
|
||||
await agreed.answer(agreed: true);
|
||||
expect(agreed.status.installed, isNotNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('cuts lines with the files installed, and only lines of the '
|
||||
'dictionary the device holds', () async {
|
||||
final dictionaries = open();
|
||||
expect(await dictionaries.wordsOf(jmdict, ['今日は']), isNull);
|
||||
|
||||
await dictionaries.download();
|
||||
final installed = dictionaries.status.installed!;
|
||||
|
||||
expect(
|
||||
(await dictionaries.wordsOf(jmdict, ['今日は']))!.single.single.text,
|
||||
'[今日は]',
|
||||
);
|
||||
expect(analyser.read, [
|
||||
'${folder.path}/${installed.ipadic}',
|
||||
'${folder.path}/${installed.database}',
|
||||
]);
|
||||
expect(await dictionaries.wordsOf(wiktionaryDe, ['Haus']), isNull);
|
||||
expect(await dictionaries.wordOfStretch(wiktionaryDe, 'Haus'), isNull);
|
||||
});
|
||||
|
||||
test('asks the instance for newer ones once 30 days have passed, '
|
||||
'downloading only what changed', () async {
|
||||
final installedAt = DateTime.utc(2026, 10, 15);
|
||||
final dictionaries = open();
|
||||
await withClock(Clock.fixed(installedAt), dictionaries.download);
|
||||
final before = dictionaries.status.installed!;
|
||||
gateway.downloaded.clear();
|
||||
final askedBefore = gateway.asked;
|
||||
|
||||
await withClock(
|
||||
Clock.fixed(installedAt.add(const Duration(days: 29))),
|
||||
() => dictionaries.wordsOf(jmdict, ['今日']),
|
||||
);
|
||||
expect(gateway.asked, askedBefore, reason: 'not due yet');
|
||||
|
||||
final newer = exportBytes([
|
||||
exportEntry(
|
||||
'1358280',
|
||||
'食べる',
|
||||
reading: 'たべる',
|
||||
version: 'fedcba9876543210',
|
||||
),
|
||||
]);
|
||||
gateway
|
||||
..offered = [
|
||||
offered('mecab-ipadic', '$ipadicUrl', pack),
|
||||
offered('jmdict-en', '$exportUrl', newer),
|
||||
]
|
||||
..bytes[exportUrl] = newer;
|
||||
final later = installedAt.add(const Duration(days: 31));
|
||||
await withClock(Clock.fixed(later), () async {
|
||||
final updated = dictionaries.changes.firstWhere(
|
||||
(state) => state.installed?.database != before.database,
|
||||
);
|
||||
await dictionaries.wordsOf(jmdict, ['今日']);
|
||||
await updated;
|
||||
});
|
||||
|
||||
final after = dictionaries.status.installed!;
|
||||
expect(gateway.downloaded, [exportUrl], reason: 'IPADIC is the same');
|
||||
expect(after.ipadic, before.ipadic);
|
||||
expect(after.checkedAt, later);
|
||||
expect(held(), {after.ipadic, after.database});
|
||||
});
|
||||
|
||||
test('removes them: lines then go through the instance again', () async {
|
||||
final dictionaries = open();
|
||||
await dictionaries.answer(agreed: true);
|
||||
|
||||
await dictionaries.remove();
|
||||
|
||||
expect(dictionaries.status.installed, isNull);
|
||||
expect(dictionaries.covers(jmdict), isFalse);
|
||||
expect(held(), isEmpty);
|
||||
expect(open().status, const DeviceDictionaryStatus(asked: true));
|
||||
});
|
||||
|
||||
group('DictionaryRepository', () {
|
||||
test('cuts lines on the device where it holds the dictionary, never '
|
||||
'sending them to the instance', () async {
|
||||
final instance = FakeDictionaryGateway(words: [word('1', '今日')]);
|
||||
final dictionaries = open();
|
||||
await dictionaries.download();
|
||||
final repository = DictionaryRepository(instance, dictionaries);
|
||||
|
||||
final [line] = await repository.wordsOf(jmdict, ['今日は']);
|
||||
final stretch = await repository.wordOfStretch(jmdict, '今日', start: 2);
|
||||
|
||||
expect(line.single.text, '[今日は]');
|
||||
expect(stretch.start, 2);
|
||||
expect(instance.keysAsked, isEmpty);
|
||||
expect(analyser.asked, [
|
||||
['今日は'],
|
||||
]);
|
||||
await repository.wordsOf(jmdict, ['今日は']);
|
||||
expect(analyser.asked, hasLength(1), reason: 'cut once');
|
||||
});
|
||||
|
||||
test('asks the instance where cutting on the device fails', () async {
|
||||
analyser = FakeJapaneseAnalyser(
|
||||
cut: (sentence) => throw StateError('No MeCab'),
|
||||
);
|
||||
final instance = FakeDictionaryGateway(words: [word('1', '今日')]);
|
||||
final dictionaries = open();
|
||||
await dictionaries.download();
|
||||
final repository = DictionaryRepository(instance, dictionaries);
|
||||
|
||||
final [line] = await repository.wordsOf(jmdict, ['今日']);
|
||||
|
||||
expect(line.single.entries.single.headword, '今日');
|
||||
expect(instance.keysAsked, isNotEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/device_dictionary_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
||||
|
||||
import '../../support/device_export.dart';
|
||||
|
||||
void main() {
|
||||
late Directory folder;
|
||||
late File export;
|
||||
late File database;
|
||||
|
||||
setUp(() {
|
||||
folder = Directory.systemTemp.createTempSync('device-dictionary');
|
||||
export = File('${folder.path}/jmdict-en.jsonl.gz');
|
||||
database = File('${folder.path}/jmdict-en.sqlite');
|
||||
});
|
||||
tearDown(() => folder.deleteSync(recursive: true));
|
||||
|
||||
test('builds a dictionary from the instance’s export and finds its words '
|
||||
'by their keys, in lower case, as the instance gives them', () async {
|
||||
writeExport(export, [
|
||||
exportEntry(
|
||||
'1358280',
|
||||
'食べる',
|
||||
reading: 'たべる',
|
||||
labels: ['common', 'jlpt:n5'],
|
||||
keys: ['食べる', 'たべる', 'Taberu'],
|
||||
glosses: ['to eat'],
|
||||
pos: ['v1', 'vt'],
|
||||
),
|
||||
exportEntry('1080510', 'テレビ', keys: ['テレビ', 'てれび']),
|
||||
]);
|
||||
|
||||
final info = await buildDeviceDictionary(export, database);
|
||||
final store = DeviceDictionaryStore.open(database);
|
||||
addTearDown(store.close);
|
||||
|
||||
expect(info.source, 'jmdict');
|
||||
expect(info.kind, DictionaryKind.words);
|
||||
expect(info.language, 'ja');
|
||||
expect(store.info, info);
|
||||
final found = store.withKeys(['たべる', 'TABERU', 'ない']);
|
||||
expect(found.keys, {'たべる', 'taberu'});
|
||||
final eat = found['たべる']!.single as DictionaryWord;
|
||||
expect(eat.ref, (source: 'jmdict', id: '1358280'));
|
||||
expect(eat.headword, '食べる');
|
||||
expect(eat.reading, 'たべる');
|
||||
expect(eat.labels, ['common', 'jlpt:n5']);
|
||||
expect(eat.version, '0123456789abcdef');
|
||||
expect(eat.senses.single.glosses, ['to eat']);
|
||||
expect(eat.senses.single.pos, ['v1', 'vt']);
|
||||
expect(found['taberu']!.single, same(eat), reason: 'made once');
|
||||
});
|
||||
|
||||
test('gives each key its first eight entries, by rank, then id', () async {
|
||||
writeExport(export, [
|
||||
for (var i = 0; i < 12; i++)
|
||||
exportEntry(
|
||||
'${1000010 - i}',
|
||||
'か$i',
|
||||
rank: i < 2 ? 100 + i : 200000,
|
||||
keys: ['か'],
|
||||
),
|
||||
]);
|
||||
await buildDeviceDictionary(export, database);
|
||||
final store = DeviceDictionaryStore.open(database);
|
||||
addTearDown(store.close);
|
||||
|
||||
expect(
|
||||
[
|
||||
for (final item in store.withKeys(['か'])['か']!) item.ref.id,
|
||||
],
|
||||
[
|
||||
'1000010',
|
||||
'1000009',
|
||||
// The rest share a rank: by id.
|
||||
'1000000',
|
||||
'1000001',
|
||||
'1000002',
|
||||
'1000003',
|
||||
'1000004',
|
||||
'1000005',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('looks up no blank key, nor one with a bar', () async {
|
||||
writeExport(export, [
|
||||
exportEntry('1', 'ね', keys: ['ね', ' ', 'a|b']),
|
||||
]);
|
||||
await buildDeviceDictionary(export, database);
|
||||
final store = DeviceDictionaryStore.open(database);
|
||||
addTearDown(store.close);
|
||||
|
||||
expect(store.withKeys([' ', 'a|b', '']), isEmpty);
|
||||
expect(store.withKeys(['ね']).keys, ['ね']);
|
||||
});
|
||||
|
||||
test('refuses an export of another format, or one that ends early, and '
|
||||
'leaves nothing behind', () async {
|
||||
writeExport(export, [
|
||||
exportEntry('1', 'ね'),
|
||||
], header: exportHeader(format: 2));
|
||||
await expectLater(
|
||||
buildDeviceDictionary(export, database),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
final whole = exportBytes([
|
||||
for (var i = 0; i < 200; i++) exportEntry('$i', 'ね$i'),
|
||||
]);
|
||||
export.writeAsBytesSync(whole.sublist(0, whole.length ~/ 2));
|
||||
await expectLater(
|
||||
buildDeviceDictionary(export, database),
|
||||
throwsA(anything),
|
||||
);
|
||||
|
||||
expect(folder.listSync().map((file) => file.path), [export.path]);
|
||||
});
|
||||
|
||||
test('opens nothing but a dictionary it built', () {
|
||||
File('${folder.path}/other.sqlite').writeAsStringSync('');
|
||||
|
||||
expect(
|
||||
() => DeviceDictionaryStore.open(File('${folder.path}/other.sqlite')),
|
||||
throwsA(anything),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.da
|
|||
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_tag_gateway.dart';
|
||||
|
||||
|
|
@ -194,7 +195,10 @@ void main() {
|
|||
word('1080510', 'テレビ', glosses: ['TV'], version: 'v1'),
|
||||
],
|
||||
);
|
||||
final updates = DictionaryUpdates(DictionaryRepository(gateway), cards);
|
||||
final updates = DictionaryUpdates(
|
||||
DictionaryRepository(gateway, noDeviceDictionaries()),
|
||||
cards,
|
||||
);
|
||||
CardContent made(String id, String? version) => CardContent(
|
||||
type: CardType.vocabulary,
|
||||
front: RichContent.plain(id),
|
||||
|
|
@ -224,7 +228,10 @@ void main() {
|
|||
word('1358280', '食べる', glosses: ['to eat'], version: 'v2'),
|
||||
],
|
||||
);
|
||||
final updates = DictionaryUpdates(DictionaryRepository(gateway), cards);
|
||||
final updates = DictionaryUpdates(
|
||||
DictionaryRepository(gateway, noDeviceDictionaries()),
|
||||
cards,
|
||||
);
|
||||
CardContent made(String version) => CardContent(
|
||||
type: CardType.vocabulary,
|
||||
front: RichContent.plain('食べる'),
|
||||
|
|
@ -310,7 +317,7 @@ void main() {
|
|||
group('the repository', () {
|
||||
test('asks for the dictionaries once, again after a failure', () async {
|
||||
final gateway = FakeDictionaryGateway()..fail = Exception('offline');
|
||||
final repository = DictionaryRepository(gateway);
|
||||
final repository = DictionaryRepository(gateway, noDeviceDictionaries());
|
||||
|
||||
await expectLater(repository.dictionaries(), throwsException);
|
||||
gateway.fail = null;
|
||||
|
|
@ -334,6 +341,7 @@ void main() {
|
|||
);
|
||||
final repository = DictionaryRepository(
|
||||
FakeDictionaryGateway(available: [kanjidic, jmdict, german]),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
expect(await repository.dictionaryFor((front: 'ja', back: 'de')), german);
|
||||
|
|
@ -359,6 +367,7 @@ void main() {
|
|||
|
||||
final all = await DictionaryRepository(
|
||||
FakeDictionaryGateway(words: words),
|
||||
noDeviceDictionaries(),
|
||||
).allLabelled(jmdict, 'jlpt:n5', onProgress: counts.add);
|
||||
|
||||
expect(all, words);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_rep
|
|||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/text_words.dart';
|
||||
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
|
||||
/// Korean words with English meanings from Wiktionary, as an instance that
|
||||
|
|
@ -52,6 +53,7 @@ void main() {
|
|||
available: const [jmdict, wiktionaryKo],
|
||||
words: koreanWords,
|
||||
),
|
||||
noDeviceDictionaries(),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.da
|
|||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/furigana.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/sentence_words.dart';
|
||||
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
|
||||
final words = [
|
||||
|
|
@ -28,6 +29,7 @@ void main() {
|
|||
const sentence = '昨日、駅で友達に会いました。';
|
||||
final repository = DictionaryRepository(
|
||||
FakeDictionaryGateway(words: words),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
final parts = (await repository.wordsOf(jmdict, [sentence])).single;
|
||||
|
|
@ -57,6 +59,7 @@ void main() {
|
|||
() async {
|
||||
final repository = DictionaryRepository(
|
||||
FakeDictionaryGateway(words: words),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
final [food, cake] = await repository.wordsOf(jmdict, [
|
||||
|
|
@ -93,6 +96,7 @@ void main() {
|
|||
word('26', '値段', reading: 'ねだん', pos: ['n'], labels: ['common']),
|
||||
],
|
||||
),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
final [particle, still, price] = await repository.wordsOf(jmdict, [
|
||||
|
|
@ -136,6 +140,7 @@ void main() {
|
|||
),
|
||||
],
|
||||
),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
final [senile, still] = await repository.wordsOf(jmdict, [
|
||||
|
|
@ -169,6 +174,7 @@ void main() {
|
|||
),
|
||||
],
|
||||
),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
final [still] = await repository.wordsOf(jmdict, ['がまだ']);
|
||||
|
|
@ -182,7 +188,10 @@ void main() {
|
|||
'asks once for a sentence, and for several sentences at once',
|
||||
() async {
|
||||
final gateway = FakeDictionaryGateway(words: words);
|
||||
final repository = DictionaryRepository(gateway);
|
||||
final repository = DictionaryRepository(
|
||||
gateway,
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
await repository.wordsOf(jmdict, ['駅で', '友達に']);
|
||||
await repository.wordsOf(jmdict, ['駅で']);
|
||||
|
|
@ -224,6 +233,7 @@ void main() {
|
|||
'out of 今日は', () async {
|
||||
final repository = DictionaryRepository(
|
||||
FakeDictionaryGateway(words: greetings),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
final [line] = await repository.wordsOf(jmdict, ['今日は']);
|
||||
expect(line.single.entries.first.headword, '今日は');
|
||||
|
|
@ -238,6 +248,7 @@ void main() {
|
|||
test('as it stands or as a form of a word: 会い of 会う', () async {
|
||||
final repository = DictionaryRepository(
|
||||
FakeDictionaryGateway(words: greetings),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
final chosen = await repository.wordOfStretch(jmdict, '会い', start: 8);
|
||||
|
|
@ -251,6 +262,7 @@ void main() {
|
|||
'punctuation', () async {
|
||||
final repository = DictionaryRepository(
|
||||
FakeDictionaryGateway(words: greetings),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
expect((await repository.wordOfStretch(jmdict, '駅で')).isWord, isFalse);
|
||||
|
|
@ -259,7 +271,7 @@ void main() {
|
|||
|
||||
test('is asked for once', () async {
|
||||
final gateway = FakeDictionaryGateway(words: greetings);
|
||||
final repository = DictionaryRepository(gateway);
|
||||
final repository = DictionaryRepository(gateway, noDeviceDictionaries());
|
||||
|
||||
await repository.wordOfStretch(jmdict, '今日');
|
||||
await repository.wordOfStretch(jmdict, '今日');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/spaced_words.
|
|||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/text_words.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/word_links.dart';
|
||||
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
|
||||
final germanWords = [
|
||||
|
|
@ -39,6 +40,7 @@ void main() {
|
|||
available: const [jmdict, wiktionaryDe],
|
||||
words: germanWords,
|
||||
),
|
||||
noDeviceDictionaries(),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -166,6 +168,7 @@ void main() {
|
|||
);
|
||||
final chineseRepository = DictionaryRepository(
|
||||
FakeDictionaryGateway(available: const [jmdict, chinese]),
|
||||
noDeviceDictionaries(),
|
||||
);
|
||||
|
||||
expect(
|
||||
|
|
|
|||
170
halkyon_learn_flutter/test/feature/dictionary/ve_words_test.dart
Normal file
170
halkyon_learn_flutter/test/feature/dictionary/ve_words_test.dart
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/mecab_tokens.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/ve_words.dart';
|
||||
|
||||
import '../../support/mecab_output.dart';
|
||||
|
||||
/// [sentence] grouped into words, from MeCab's output for it.
|
||||
List<VeWord> wordsOf(String sentence) =>
|
||||
veWords(mecabTokens(sentence, mecabOutput[sentence]!));
|
||||
|
||||
/// Each word as it stands, then its dictionary form where that differs.
|
||||
List<String> grouped(String sentence) => [
|
||||
for (final word in wordsOf(sentence))
|
||||
if (word.lemma == word.surface)
|
||||
word.surface
|
||||
else
|
||||
'${word.surface}=${word.lemma}',
|
||||
];
|
||||
|
||||
void main() {
|
||||
group('mecabTokens', () {
|
||||
test('reads each token with its part of speech, conjugation, dictionary '
|
||||
'form and reading, where it stands in the text', () {
|
||||
const sentence = '食べられなかった';
|
||||
final tokens = mecabTokens(sentence, mecabOutput[sentence]!);
|
||||
|
||||
expect(
|
||||
[for (final token in tokens) token.surface],
|
||||
[
|
||||
'食べ',
|
||||
'られ',
|
||||
'なかっ',
|
||||
'た',
|
||||
],
|
||||
);
|
||||
expect(
|
||||
tokens.first,
|
||||
const MecabToken(
|
||||
surface: '食べ',
|
||||
start: 0,
|
||||
pos: '動詞',
|
||||
pos2: '自立',
|
||||
conjugationType: '一段',
|
||||
conjugationForm: '未然形',
|
||||
lemma: '食べる',
|
||||
reading: 'タベ',
|
||||
),
|
||||
);
|
||||
expect([for (final token in tokens) token.start], [0, 2, 4, 7]);
|
||||
expect(tokens.last.end, sentence.length);
|
||||
});
|
||||
|
||||
test('places tokens after spaces MeCab skipped, and characters beyond '
|
||||
'the basic plane as Dart counts them', () {
|
||||
const spaced = '食べて 寝た';
|
||||
expect(
|
||||
[
|
||||
for (final token in mecabTokens(spaced, mecabOutput[spaced]!))
|
||||
token.start,
|
||||
],
|
||||
[0, 2, 4, 5],
|
||||
);
|
||||
|
||||
const scolded = '𠮟られた';
|
||||
final tokens = mecabTokens(scolded, mecabOutput[scolded]!);
|
||||
expect([for (final token in tokens) token.start], [0, 2, 4]);
|
||||
expect(tokens.first.end, 2);
|
||||
});
|
||||
|
||||
test('takes a word IPADIC lacks as its own dictionary form, without a '
|
||||
'reading', () {
|
||||
const sentence = 'OKだよ';
|
||||
final ok = mecabTokens(sentence, mecabOutput[sentence]!).first;
|
||||
|
||||
expect(ok.surface, 'OK');
|
||||
expect(ok.lemma, 'OK');
|
||||
expect(ok.reading, isNull);
|
||||
});
|
||||
|
||||
test('leaves out what is no token: the end, and empty lines', () {
|
||||
expect(mecabTokens('', 'EOS\n'), isEmpty);
|
||||
expect(mecabTokens('今日', '\n今日\t名詞,副詞可能,*,*,*,*,今日\nEOS'), hasLength(1));
|
||||
});
|
||||
});
|
||||
|
||||
group('veWords', () {
|
||||
test('joins a verb with its endings and auxiliaries, as its dictionary '
|
||||
'form', () {
|
||||
expect(grouped('食べられなかった'), ['食べられなかった=食べる']);
|
||||
expect(grouped('見ていなかった'), ['見ていなかった=見る']);
|
||||
expect(grouped('学校に行きます'), ['学校', 'に', '行きます=行く']);
|
||||
expect(grouped('読めば分かる'), ['読めば=読む', '分かる']);
|
||||
expect(grouped('食べて、寝た'), ['食べて=食べる', '、', '寝た=寝る']);
|
||||
});
|
||||
|
||||
test('joins a noun with the する it takes, as a verb', () {
|
||||
final [study] = wordsOf('勉強している');
|
||||
|
||||
expect(study.surface, '勉強している');
|
||||
expect(study.lemma, '勉強する');
|
||||
expect(study.pos, VePos.verb);
|
||||
expect(grouped('勉強しなければならない'), ['勉強しなければならない=勉強する']);
|
||||
});
|
||||
|
||||
test('joins a na-adjective with its な, as the adjective, and a ない '
|
||||
'adjective with its ない', () {
|
||||
final [quiet, room] = wordsOf('静かな部屋');
|
||||
|
||||
expect(quiet.surface, '静かな');
|
||||
expect(quiet.lemma, '静か');
|
||||
expect(quiet.pos, VePos.adjective);
|
||||
expect(room.surface, '部屋');
|
||||
expect(grouped('申し訳ない'), ['申し訳ない']);
|
||||
});
|
||||
|
||||
test('keeps particles, the copula and です words of their own, but for '
|
||||
'て, で and ば after a verb', () {
|
||||
expect(grouped('今日はいい天気ですね'), ['今日', 'は', 'いい', '天気', 'です', 'ね']);
|
||||
expect(grouped('お前がもうろくだよ'), ['お前', 'が', 'もうろく', 'だ', 'よ']);
|
||||
expect(grouped('彼は先生だ'), ['彼', 'は', '先生', 'だ']);
|
||||
expect(grouped('東京都に住んでいます'), ['東京都', 'に', '住んでいます=住む']);
|
||||
expect(grouped('がまだ'), ['が', 'まだ']);
|
||||
expect(grouped('綺麗じゃない'), ['綺麗', 'じゃない=じゃ']);
|
||||
});
|
||||
|
||||
test('joins a suffix to its noun, but keeps one after a name apart', () {
|
||||
expect(grouped('子供たちが遊んでいる'), ['子供たち', 'が', '遊んでいる=遊ぶ']);
|
||||
expect(grouped('田中さんが来た'), ['田中', 'さん', 'が', '来た=来る']);
|
||||
});
|
||||
|
||||
test('joins numbers with each other and their counter', () {
|
||||
expect(grouped('三人で行った'), ['三人', 'で', '行った=行う']);
|
||||
expect(grouped('二十五歳です'), ['二十五歳', 'です']);
|
||||
});
|
||||
|
||||
test('joins ため with its に, and keeps symbols apart', () {
|
||||
expect(grouped('私のために'), ['私', 'の', 'ために']);
|
||||
final words = wordsOf('「おはよう」と言った');
|
||||
expect(
|
||||
[for (final word in words) word.surface],
|
||||
[
|
||||
'「',
|
||||
'おはよう',
|
||||
'」',
|
||||
'と',
|
||||
'言った',
|
||||
],
|
||||
);
|
||||
expect(words.first.pos, VePos.symbol);
|
||||
expect(words[1].pos, VePos.interjection);
|
||||
});
|
||||
|
||||
test('keeps two nouns MeCab cut apart as two words', () {
|
||||
expect(grouped('携帯電話を買った'), ['携帯', '電話', 'を', '買った=買う']);
|
||||
});
|
||||
|
||||
test('places each word where it stands', () {
|
||||
final words = wordsOf('学校に行きます');
|
||||
|
||||
expect(
|
||||
[for (final word in words) (word.start, word.end)],
|
||||
[
|
||||
(0, 2),
|
||||
(2, 3),
|
||||
(3, 7),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -8,8 +8,11 @@ import 'package:halkyon_learn_flutter/src/core/video/media_players.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/widget/vocabulary_senses.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/device_dictionaries.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_adder.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/device_dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/sentence_words.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/model/jellyfin.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/widget/watch_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/widget/study_screen.dart';
|
||||
|
|
@ -19,6 +22,7 @@ import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
|||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_jellyfin.dart';
|
||||
import '../../support/fake_review_gateway.dart';
|
||||
|
|
@ -62,6 +66,8 @@ void main() {
|
|||
FakeReviewGateway? reviews,
|
||||
FakeJellyfinGateway? gateway,
|
||||
String itemId = 'episode-1',
|
||||
FakeDictionaryGateway? dictionary,
|
||||
DeviceDictionaries? device,
|
||||
}) async {
|
||||
if (gateway != null) {
|
||||
jellyfin = gateway;
|
||||
|
|
@ -101,9 +107,12 @@ void main() {
|
|||
studysetGateway: studysets,
|
||||
cardGateway: cardGateway,
|
||||
tagGateway: tagGateway,
|
||||
dictionaryGateway: words == null
|
||||
? sampleDictionary()
|
||||
: FakeDictionaryGateway(words: words),
|
||||
dictionaryGateway:
|
||||
dictionary ??
|
||||
(words == null
|
||||
? sampleDictionary()
|
||||
: FakeDictionaryGateway(words: words)),
|
||||
deviceDictionaries: device,
|
||||
reviewGateway: reviews,
|
||||
jellyfinGateway: jellyfin,
|
||||
jellyfinStore: signedInJellyfin(),
|
||||
|
|
@ -144,6 +153,54 @@ void main() {
|
|||
expect(find.text('友達'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('offers once to look words up on the device, saying how much '
|
||||
'it downloads; Not now offers it no more', (tester) async {
|
||||
final device = deviceDictionariesIn(
|
||||
FakeDeviceDictionaryGateway(offered: offeredJapanese()),
|
||||
);
|
||||
await watch(tester, device: device);
|
||||
|
||||
expect(find.text('Look words up on this device?'), findsOneWidget);
|
||||
expect(find.textContaining('(34.0 MB, more once set up)'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Not now'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Look words up on this device?'), findsNothing);
|
||||
expect(device.status.asked, isTrue);
|
||||
expect(device.status.progress, isNull);
|
||||
});
|
||||
|
||||
testWidgets('with Japanese’s dictionaries on the device, lines are cut '
|
||||
'there, and the instance never sees them', (tester) async {
|
||||
final analyser = FakeJapaneseAnalyser(
|
||||
cut: (sentence) => [
|
||||
SentencePart(text: sentence, start: 0, entries: [sampleWords.first]),
|
||||
],
|
||||
);
|
||||
final dictionary = sampleDictionary();
|
||||
await watch(
|
||||
tester,
|
||||
dictionary: dictionary,
|
||||
device: deviceDictionariesIn(
|
||||
FakeDeviceDictionaryGateway(offered: offeredJapanese()),
|
||||
analyser: analyser,
|
||||
initial: DeviceDictionaryStatus(
|
||||
installed: installedJapanese(),
|
||||
asked: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await playTo(tester, const Duration(seconds: 2));
|
||||
|
||||
expect(
|
||||
analyser.asked.expand((sentences) => sentences),
|
||||
contains('昨日、駅で友達に会いました。'),
|
||||
);
|
||||
expect(dictionary.keysAsked, isEmpty);
|
||||
expect(find.text('Look words up on this device?'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a word tapped pauses the video and shows what it is a form '
|
||||
"of; added, it is a flashcard with the dictionary's checked examples "
|
||||
'(never its line, which may be wrong), tagged with the show and the '
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/device_dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../../support/fake_device_dictionaries.dart';
|
||||
import '../../../support/fake_settings_gateway.dart';
|
||||
import '../../../support/memory_device_settings_store.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
|
|
@ -50,6 +54,69 @@ void main() {
|
|||
expect(find.text('Version 1.2.3 (build 45)'), findsOneWidget);
|
||||
});
|
||||
|
||||
group('Japanese on this device', () {
|
||||
/// Opens Settings, tall enough to show every card at once.
|
||||
Future<void> openSettings(
|
||||
WidgetTester tester,
|
||||
FakeDeviceDictionaryGateway gateway, {
|
||||
DeviceDictionaryStatus initial = const DeviceDictionaryStatus(),
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(800, 5000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
deviceDictionaries: deviceDictionariesIn(gateway, initial: initial),
|
||||
),
|
||||
location: RoutePaths.settings,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('downloads the dictionaries the instance offers devices, and '
|
||||
'says when that failed', (tester) async {
|
||||
final gateway = FakeDeviceDictionaryGateway(offered: offeredJapanese());
|
||||
await openSettings(tester, gateway);
|
||||
|
||||
expect(find.text('Japanese on this device'), findsOneWidget);
|
||||
gateway.fail = const SocketException('offline');
|
||||
await tester.tap(find.text('Download (34.0 MB)'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(gateway.asked, 2);
|
||||
expect(find.text('The download failed.'), findsOneWidget);
|
||||
expect(find.text('Download (34.0 MB)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('says how much room they take, and removes them', (
|
||||
tester,
|
||||
) async {
|
||||
await openSettings(
|
||||
tester,
|
||||
FakeDeviceDictionaryGateway(offered: offeredJapanese()),
|
||||
initial: DeviceDictionaryStatus(
|
||||
installed: installedJapanese(),
|
||||
asked: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('On this device: 150.0 MB'), findsOneWidget);
|
||||
await tester.tap(find.text('Remove'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('On this device: 150.0 MB'), findsNothing);
|
||||
expect(find.text('Download (34.0 MB)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows nothing of them where the instance offers none and '
|
||||
'the device holds none', (tester) async {
|
||||
await openSettings(tester, FakeDeviceDictionaryGateway());
|
||||
|
||||
expect(find.text('Japanese on this device'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('a device that never chose follows its own theme', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
70
halkyon_learn_flutter/test/support/device_export.dart
Normal file
70
halkyon_learn_flutter/test/support/device_export.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
/// The header of an export of JMdict, as the instance writes it.
|
||||
Map<String, Object?> exportHeader({Object format = 1, String kind = 'words'}) =>
|
||||
{
|
||||
'format': format,
|
||||
'source': 'jmdict',
|
||||
'kind': kind,
|
||||
'language': 'ja',
|
||||
'glossLanguage': 'en',
|
||||
'sourceVersion': '3.6.2 (2026-10-05)',
|
||||
'license': 'CC-BY-SA-4.0',
|
||||
'credits': [
|
||||
{
|
||||
'name': 'JMdict',
|
||||
'by': 'EDRDG',
|
||||
'license': 'CC-BY-SA-4.0',
|
||||
'url': 'https://www.edrdg.org/edrdg/licence.html',
|
||||
},
|
||||
],
|
||||
'entryCount': 0,
|
||||
};
|
||||
|
||||
/// An entry of an export, as the instance writes it.
|
||||
Map<String, Object?> exportEntry(
|
||||
String id,
|
||||
String headword, {
|
||||
String? reading,
|
||||
int rank = 100000,
|
||||
List<String> labels = const [],
|
||||
List<String> keys = const [],
|
||||
List<String> glosses = const ['a gloss'],
|
||||
List<String> pos = const ['n'],
|
||||
String version = '0123456789abcdef',
|
||||
}) => {
|
||||
'id': id,
|
||||
'headword': headword,
|
||||
'reading': ?reading,
|
||||
'rank': rank,
|
||||
'labels': labels,
|
||||
'entry': jsonEncode({
|
||||
'senses': [
|
||||
{'glosses': glosses, 'pos': pos},
|
||||
],
|
||||
}),
|
||||
'version': version,
|
||||
'keys': keys.isEmpty ? [headword, ?reading] : keys,
|
||||
};
|
||||
|
||||
/// The export of [entries] after [header], as the instance serves it:
|
||||
/// gzipped JSON lines.
|
||||
List<int> exportBytes(
|
||||
List<Map<String, Object?>> entries, {
|
||||
Map<String, Object?>? header,
|
||||
}) => gzip.encode(
|
||||
utf8.encode(
|
||||
[
|
||||
jsonEncode(header ?? exportHeader()),
|
||||
for (final entry in entries) jsonEncode(entry),
|
||||
].join('\n'),
|
||||
),
|
||||
);
|
||||
|
||||
/// Writes the export of [entries] into [file].
|
||||
File writeExport(
|
||||
File file,
|
||||
List<Map<String, Object?>> entries, {
|
||||
Map<String, Object?>? header,
|
||||
}) => file..writeAsBytesSync(exportBytes(entries, header: header));
|
||||
137
halkyon_learn_flutter/test/support/fake_device_dictionaries.dart
Normal file
137
halkyon_learn_flutter/test/support/fake_device_dictionaries.dart
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/device_dictionaries.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/device_dictionary_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/japanese_analyser.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/device_dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/sentence_words.dart';
|
||||
|
||||
/// An instance offering [offered] for devices, each file's bytes by its
|
||||
/// address in [bytes]; offering none by default.
|
||||
class FakeDeviceDictionaryGateway({
|
||||
var List<DeviceFile> offered = const [],
|
||||
final Map<Uri, List<int>> bytes = const {},
|
||||
}) implements DeviceDictionaryGateway {
|
||||
/// The addresses downloaded, in order.
|
||||
final downloaded = <Uri>[];
|
||||
|
||||
/// How many times the files were asked for.
|
||||
int asked = 0;
|
||||
|
||||
/// Thrown by [files] while set: the instance can't be reached.
|
||||
Exception? fail;
|
||||
|
||||
@override
|
||||
Future<List<DeviceFile>> files(String language) async {
|
||||
asked++;
|
||||
if (fail case final error?) {
|
||||
throw error;
|
||||
}
|
||||
return language == 'ja' ? offered : const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<int>> download(Uri url) async* {
|
||||
downloaded.add(url);
|
||||
final all = bytes[url] ?? (throw HttpException('Not found', uri: url));
|
||||
// In chunks, as a download comes.
|
||||
for (var at = 0; at < all.length; at += 4096) {
|
||||
yield all.sublist(at, at + 4096 > all.length ? all.length : at + 4096);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cuts each sentence into the parts [cut] gives, and looks a stretch up
|
||||
/// as [stretch] gives it; records what it was asked.
|
||||
class FakeJapaneseAnalyser({
|
||||
final List<SentencePart> Function(String sentence)? cut,
|
||||
final SentencePart Function(String text, int start)? stretch,
|
||||
}) implements JapaneseAnalyser {
|
||||
/// The sentences asked for, each call's.
|
||||
final asked = <List<String>>[];
|
||||
|
||||
/// The folders and databases asked to read.
|
||||
final read = <String>[];
|
||||
|
||||
@override
|
||||
Future<List<List<SentencePart>>> wordsOf({
|
||||
required String ipadic,
|
||||
required String database,
|
||||
required List<String> sentences,
|
||||
}) async {
|
||||
asked.add(sentences);
|
||||
read
|
||||
..add(ipadic)
|
||||
..add(database);
|
||||
return [
|
||||
for (final sentence in sentences)
|
||||
cut?.call(sentence) ?? [SentencePart(text: sentence, start: 0)],
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SentencePart> wordOfStretch({
|
||||
required String database,
|
||||
required String text,
|
||||
required int start,
|
||||
}) async {
|
||||
read.add(database);
|
||||
return stretch?.call(text, start) ?? SentencePart(text: text, start: start);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dictionaries on a device that holds none, nor is offered any: lines are
|
||||
/// cut through the instance.
|
||||
DeviceDictionaries noDeviceDictionaries() => DeviceDictionaries(
|
||||
FakeDeviceDictionaryGateway(),
|
||||
FakeJapaneseAnalyser(),
|
||||
folder: Directory('${Directory.systemTemp.path}/halkyon-no-dictionaries'),
|
||||
);
|
||||
|
||||
/// The files an instance offers devices for Japanese, IPADIC and JMdict, as
|
||||
/// large as learn.halkyon.eu's would be: 34.0 MB together.
|
||||
List<DeviceFile> offeredJapanese() => [
|
||||
DeviceFile(
|
||||
name: 'mecab-ipadic',
|
||||
url: Uri.parse('https://learn.example.org/device-dictionaries/ipadic'),
|
||||
sha256: '11' * 32,
|
||||
size: 13953908,
|
||||
),
|
||||
DeviceFile(
|
||||
name: 'jmdict-en',
|
||||
url: Uri.parse('https://learn.example.org/device-dictionaries/jmdict'),
|
||||
sha256: '22' * 32,
|
||||
size: 21749430,
|
||||
),
|
||||
];
|
||||
|
||||
/// What a device holds once it set them up: 150.0 MB, checked just now.
|
||||
DeviceInstallation installedJapanese() => DeviceInstallation(
|
||||
ipadic: 'ipadic-1111111111111111',
|
||||
database: 'jmdict-en-2222222222222222.sqlite',
|
||||
source: 'jmdict',
|
||||
glossLanguage: 'en',
|
||||
files: {'mecab-ipadic': '11' * 32, 'jmdict-en': '22' * 32},
|
||||
bytes: 150 * 1024 * 1024,
|
||||
installedAt: clock.now().toUtc(),
|
||||
checkedAt: clock.now().toUtc(),
|
||||
);
|
||||
|
||||
/// Dictionaries on a device, kept in a folder of their own that goes after
|
||||
/// the test, offered by [gateway] and cut with [analyser].
|
||||
DeviceDictionaries deviceDictionariesIn(
|
||||
FakeDeviceDictionaryGateway gateway, {
|
||||
FakeJapaneseAnalyser? analyser,
|
||||
DeviceDictionaryStatus initial = const DeviceDictionaryStatus(),
|
||||
}) {
|
||||
final folder = Directory.systemTemp.createTempSync('dictionaries');
|
||||
addTearDown(() => folder.deleteSync(recursive: true));
|
||||
return DeviceDictionaries(
|
||||
gateway,
|
||||
analyser ?? FakeJapaneseAnalyser(),
|
||||
folder: folder,
|
||||
initial: initial,
|
||||
);
|
||||
}
|
||||
174
halkyon_learn_flutter/test/support/mecab_output.dart
Normal file
174
halkyon_learn_flutter/test/support/mecab_output.dart
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/// MeCab's output with IPADIC (MeCab 0.996, Ubuntu's `mecab-ipadic-utf8`)
|
||||
/// for each sentence, as the app's MeCab gives it.
|
||||
const mecabOutput = {
|
||||
'お前がもうろくだよ':
|
||||
'お前\t名詞,代名詞,一般,*,*,*,お前,オマエ,オマエ\n'
|
||||
'が\t助詞,格助詞,一般,*,*,*,が,ガ,ガ\n'
|
||||
'もうろく\t名詞,サ変接続,*,*,*,*,もうろく,モウロク,モーロク\n'
|
||||
'だ\t助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ\n'
|
||||
'よ\t助詞,終助詞,*,*,*,*,よ,ヨ,ヨ\n'
|
||||
'EOS\n',
|
||||
'今日はいい天気ですね':
|
||||
'今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー\n'
|
||||
'は\t助詞,係助詞,*,*,*,*,は,ハ,ワ\n'
|
||||
'いい\t形容詞,自立,*,*,形容詞・イイ,基本形,いい,イイ,イイ\n'
|
||||
'天気\t名詞,一般,*,*,*,*,天気,テンキ,テンキ\n'
|
||||
'です\t助動詞,*,*,*,特殊・デス,基本形,です,デス,デス\n'
|
||||
'ね\t助詞,終助詞,*,*,*,*,ね,ネ,ネ\n'
|
||||
'EOS\n',
|
||||
'食べられなかった':
|
||||
'食べ\t動詞,自立,*,*,一段,未然形,食べる,タベ,タベ\n'
|
||||
'られ\t動詞,接尾,*,*,一段,未然形,られる,ラレ,ラレ\n'
|
||||
'なかっ\t助動詞,*,*,*,特殊・ナイ,連用タ接続,ない,ナカッ,ナカッ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
'勉強している':
|
||||
'勉強\t名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー\n'
|
||||
'し\t動詞,自立,*,*,サ変・スル,連用形,する,シ,シ\n'
|
||||
'て\t助詞,接続助詞,*,*,*,*,て,テ,テ\n'
|
||||
'いる\t動詞,非自立,*,*,一段,基本形,いる,イル,イル\n'
|
||||
'EOS\n',
|
||||
'学校に行きます':
|
||||
'学校\t名詞,一般,*,*,*,*,学校,ガッコウ,ガッコー\n'
|
||||
'に\t助詞,格助詞,一般,*,*,*,に,ニ,ニ\n'
|
||||
'行き\t動詞,自立,*,*,五段・カ行促音便,連用形,行く,イキ,イキ\n'
|
||||
'ます\t助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス\n'
|
||||
'EOS\n',
|
||||
'静かな部屋':
|
||||
'静か\t名詞,形容動詞語幹,*,*,*,*,静か,シズカ,シズカ\n'
|
||||
'な\t助動詞,*,*,*,特殊・ダ,体言接続,だ,ナ,ナ\n'
|
||||
'部屋\t名詞,一般,*,*,*,*,部屋,ヘヤ,ヘヤ\n'
|
||||
'EOS\n',
|
||||
'日本語を話せますか':
|
||||
'日本語\t名詞,一般,*,*,*,*,日本語,ニホンゴ,ニホンゴ\n'
|
||||
'を\t助詞,格助詞,一般,*,*,*,を,ヲ,ヲ\n'
|
||||
'話せ\t動詞,自立,*,*,一段,連用形,話せる,ハナセ,ハナセ\n'
|
||||
'ます\t助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス\n'
|
||||
'か\t助詞,副助詞/並立助詞/終助詞,*,*,*,*,か,カ,カ\n'
|
||||
'EOS\n',
|
||||
'子供たちが遊んでいる':
|
||||
'子供\t名詞,一般,*,*,*,*,子供,コドモ,コドモ\n'
|
||||
'たち\t名詞,接尾,一般,*,*,*,たち,タチ,タチ\n'
|
||||
'が\t助詞,格助詞,一般,*,*,*,が,ガ,ガ\n'
|
||||
'遊ん\t動詞,自立,*,*,五段・バ行,連用タ接続,遊ぶ,アソン,アソン\n'
|
||||
'で\t助詞,接続助詞,*,*,*,*,で,デ,デ\n'
|
||||
'いる\t動詞,非自立,*,*,一段,基本形,いる,イル,イル\n'
|
||||
'EOS\n',
|
||||
'三人で行った':
|
||||
'三\t名詞,数,*,*,*,*,三,サン,サン\n'
|
||||
'人\t名詞,接尾,助数詞,*,*,*,人,ニン,ニン\n'
|
||||
'で\t助詞,格助詞,一般,*,*,*,で,デ,デ\n'
|
||||
'行っ\t動詞,自立,*,*,五段・ワ行促音便,連用タ接続,行う,オコナッ,オコナッ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
'私のために':
|
||||
'私\t名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ\n'
|
||||
'の\t助詞,連体化,*,*,*,*,の,ノ,ノ\n'
|
||||
'ため\t名詞,非自立,副詞可能,*,*,*,ため,タメ,タメ\n'
|
||||
'に\t助詞,格助詞,一般,*,*,*,に,ニ,ニ\n'
|
||||
'EOS\n',
|
||||
'彼は先生だ':
|
||||
'彼\t名詞,代名詞,一般,*,*,*,彼,カレ,カレ\n'
|
||||
'は\t助詞,係助詞,*,*,*,*,は,ハ,ワ\n'
|
||||
'先生\t名詞,一般,*,*,*,*,先生,センセイ,センセイ\n'
|
||||
'だ\t助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ\n'
|
||||
'EOS\n',
|
||||
'食べて、寝た':
|
||||
'食べ\t動詞,自立,*,*,一段,連用形,食べる,タベ,タベ\n'
|
||||
'て\t助詞,接続助詞,*,*,*,*,て,テ,テ\n'
|
||||
'、\t記号,読点,*,*,*,*,、,、,、\n'
|
||||
'寝\t動詞,自立,*,*,一段,連用形,寝る,ネ,ネ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
'読めば分かる':
|
||||
'読め\t動詞,自立,*,*,五段・マ行,仮定形,読む,ヨメ,ヨメ\n'
|
||||
'ば\t助詞,接続助詞,*,*,*,*,ば,バ,バ\n'
|
||||
'分かる\t動詞,自立,*,*,五段・ラ行,基本形,分かる,ワカル,ワカル\n'
|
||||
'EOS\n',
|
||||
'綺麗じゃない':
|
||||
'綺麗\t名詞,形容動詞語幹,*,*,*,*,綺麗,キレイ,キレイ\n'
|
||||
'じゃ\t助詞,副助詞,*,*,*,*,じゃ,ジャ,ジャ\n'
|
||||
'ない\t助動詞,*,*,*,特殊・ナイ,基本形,ない,ナイ,ナイ\n'
|
||||
'EOS\n',
|
||||
'東京都に住んでいます':
|
||||
'東京\t名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー\n'
|
||||
'都\t名詞,接尾,地域,*,*,*,都,ト,ト\n'
|
||||
'に\t助詞,格助詞,一般,*,*,*,に,ニ,ニ\n'
|
||||
'住ん\t動詞,自立,*,*,五段・マ行,連用タ接続,住む,スン,スン\n'
|
||||
'で\t助詞,接続助詞,*,*,*,*,で,デ,デ\n'
|
||||
'い\t動詞,非自立,*,*,一段,連用形,いる,イ,イ\n'
|
||||
'ます\t助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス\n'
|
||||
'EOS\n',
|
||||
'「おはよう」と言った':
|
||||
'「\t記号,括弧開,*,*,*,*,「,「,「\n'
|
||||
'おはよう\t感動詞,*,*,*,*,*,おはよう,オハヨウ,オハヨー\n'
|
||||
'」\t記号,括弧閉,*,*,*,*,」,」,」\n'
|
||||
'と\t助詞,格助詞,引用,*,*,*,と,ト,ト\n'
|
||||
'言っ\t動詞,自立,*,*,五段・ワ行促音便,連用タ接続,言う,イッ,イッ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
'OKだよ':
|
||||
'OK\t名詞,一般,*,*,*,*,*\n'
|
||||
'だ\t助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ\n'
|
||||
'よ\t助詞,終助詞,*,*,*,*,よ,ヨ,ヨ\n'
|
||||
'EOS\n',
|
||||
'田中さんが来た':
|
||||
'田中\t名詞,固有名詞,人名,姓,*,*,田中,タナカ,タナカ\n'
|
||||
'さん\t名詞,接尾,人名,*,*,*,さん,サン,サン\n'
|
||||
'が\t助詞,格助詞,一般,*,*,*,が,ガ,ガ\n'
|
||||
'来\t動詞,自立,*,*,カ変・来ル,連用形,来る,キ,キ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
'本当にすごい':
|
||||
'本当に\t副詞,一般,*,*,*,*,本当に,ホントウニ,ホントーニ\n'
|
||||
'すごい\t形容詞,自立,*,*,形容詞・アウオ段,基本形,すごい,スゴイ,スゴイ\n'
|
||||
'EOS\n',
|
||||
'がまだ':
|
||||
'が\t助詞,格助詞,一般,*,*,*,が,ガ,ガ\n'
|
||||
'まだ\t副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ\n'
|
||||
'EOS\n',
|
||||
'二十五歳です':
|
||||
'二\t名詞,数,*,*,*,*,二,ニ,ニ\n'
|
||||
'十\t名詞,数,*,*,*,*,十,ジュウ,ジュー\n'
|
||||
'五\t名詞,数,*,*,*,*,五,ゴ,ゴ\n'
|
||||
'歳\t名詞,接尾,助数詞,*,*,*,歳,サイ,サイ\n'
|
||||
'です\t助動詞,*,*,*,特殊・デス,基本形,です,デス,デス\n'
|
||||
'EOS\n',
|
||||
'勉強しなければならない':
|
||||
'勉強\t名詞,サ変接続,*,*,*,*,勉強,ベンキョウ,ベンキョー\n'
|
||||
'し\t動詞,自立,*,*,サ変・スル,未然形,する,シ,シ\n'
|
||||
'なけれ\t助動詞,*,*,*,特殊・ナイ,仮定形,ない,ナケレ,ナケレ\n'
|
||||
'ば\t助詞,接続助詞,*,*,*,*,ば,バ,バ\n'
|
||||
'なら\t動詞,非自立,*,*,五段・ラ行,未然形,なる,ナラ,ナラ\n'
|
||||
'ない\t助動詞,*,*,*,特殊・ナイ,基本形,ない,ナイ,ナイ\n'
|
||||
'EOS\n',
|
||||
'見ていなかった':
|
||||
'見\t動詞,自立,*,*,一段,連用形,見る,ミ,ミ\n'
|
||||
'て\t助詞,接続助詞,*,*,*,*,て,テ,テ\n'
|
||||
'い\t動詞,非自立,*,*,一段,未然形,いる,イ,イ\n'
|
||||
'なかっ\t助動詞,*,*,*,特殊・ナイ,連用タ接続,ない,ナカッ,ナカッ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
'携帯電話を買った':
|
||||
'携帯\t名詞,サ変接続,*,*,*,*,携帯,ケイタイ,ケイタイ\n'
|
||||
'電話\t名詞,サ変接続,*,*,*,*,電話,デンワ,デンワ\n'
|
||||
'を\t助詞,格助詞,一般,*,*,*,を,ヲ,ヲ\n'
|
||||
'買っ\t動詞,自立,*,*,五段・ワ行促音便,連用タ接続,買う,カッ,カッ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
'食べて 寝た':
|
||||
'食べ\t動詞,自立,*,*,一段,連用形,食べる,タベ,タベ\n'
|
||||
'て\t助詞,接続助詞,*,*,*,*,て,テ,テ\n'
|
||||
'寝\t動詞,自立,*,*,一段,連用形,寝る,ネ,ネ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
'申し訳ない':
|
||||
'申し訳\t名詞,ナイ形容詞語幹,*,*,*,*,申し訳,モウシワケ,モーシワケ\n'
|
||||
'ない\t助動詞,*,*,*,特殊・ナイ,基本形,ない,ナイ,ナイ\n'
|
||||
'EOS\n',
|
||||
'𠮟られた':
|
||||
'𠮟\t記号,一般,*,*,*,*,*\n'
|
||||
'られ\t動詞,接尾,*,*,一段,連用形,られる,ラレ,ラレ\n'
|
||||
'た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\n'
|
||||
'EOS\n',
|
||||
};
|
||||
|
|
@ -33,6 +33,7 @@ import 'package:halkyon_learn_flutter/src/feature/auth/data/sign_in_addresses.da
|
|||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/device_dictionaries.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/explore/data/copy_updater.dart';
|
||||
|
|
@ -85,6 +86,7 @@ import 'fake_asset_server_gateway.dart';
|
|||
import 'fake_assistant_gateway.dart';
|
||||
import 'fake_auth_gateway.dart';
|
||||
import 'fake_card_gateway.dart';
|
||||
import 'fake_device_dictionaries.dart';
|
||||
import 'fake_dictionary_gateway.dart';
|
||||
import 'fake_explore_gateway.dart';
|
||||
import 'fake_feedback_gateways.dart';
|
||||
|
|
@ -150,6 +152,7 @@ Dependencies testDependencies({
|
|||
AssetFileCache? assetFiles,
|
||||
AboutGateway? aboutGateway,
|
||||
DictionaryGateway? dictionaryGateway,
|
||||
DeviceDictionaries? deviceDictionaries,
|
||||
DeviceSettingsStore? deviceSettingsStore,
|
||||
DeviceSettings deviceSettings = const DeviceSettings(),
|
||||
AppVersion appVersion = const AppVersion(version: '1.0.0', build: '1'),
|
||||
|
|
@ -172,187 +175,194 @@ Dependencies testDependencies({
|
|||
JellyfinStore? jellyfinStore,
|
||||
JellyfinSuggestion? jellyfinSuggestion,
|
||||
MediaPlayers? mediaPlayers,
|
||||
}) => Dependencies(
|
||||
serverName: 'learn.example.org',
|
||||
auth: AuthRepository(
|
||||
authGateway ?? FakeAuthGateway(),
|
||||
signInAddresses ?? MemorySignInAddresses(),
|
||||
),
|
||||
about: AboutRepository(aboutGateway ?? FakeAboutGateway()),
|
||||
dictionaries: DictionaryRepository(
|
||||
dictionaryGateway ?? FakeDictionaryGateway(),
|
||||
),
|
||||
maps: maps ?? GeoMapRepository(),
|
||||
strokes: strokes ?? StrokeRepository(),
|
||||
sounds: sounds ?? FakeSoundPlayer(),
|
||||
files: files ?? FakeFilePicker(),
|
||||
fileSaver: fileSaver ?? FakeFileSaver(),
|
||||
videos: videos ?? FakeYoutubeVideos(),
|
||||
pdfEngine: pdfEngine ?? FakePdfEngine(),
|
||||
ankiPackages: InlineAnkiPackageReader(),
|
||||
archives: InlineArchiveReader(),
|
||||
exportArchiver: InlineExportArchiver(),
|
||||
deviceSettingsStore:
|
||||
deviceSettingsStore ?? MemoryDeviceSettingsStore(deviceSettings),
|
||||
deviceSettings: deviceSettings,
|
||||
appVersion: appVersion,
|
||||
pomodoro: pomodoro ?? FakePomodoroPlatform(),
|
||||
assistantHandOff:
|
||||
assistantHandOff ?? AssistantHandOff(open: (url) async => true),
|
||||
webSearch:
|
||||
webSearch ??
|
||||
WebSearch(
|
||||
open: (url) async => true,
|
||||
deviceSearch: (query) async => false,
|
||||
),
|
||||
assistant: AssistantRepository(assistantGateway ?? FakeAssistantGateway()),
|
||||
jellyfin: JellyfinRepository(
|
||||
(device) => jellyfinGateway ?? FakeJellyfinGateway(),
|
||||
jellyfinStore ?? MemoryJellyfinStore(),
|
||||
deviceName: 'Test',
|
||||
appVersion: appVersion.version,
|
||||
),
|
||||
jellyfinSuggestion:
|
||||
jellyfinSuggestion ?? JellyfinSuggestion(() async => null),
|
||||
mediaPlayers: mediaPlayers ?? FakeMediaPlayers(),
|
||||
openPerson:
|
||||
openPerson ??
|
||||
(person) async {
|
||||
final sync = SyncService(
|
||||
syncOnce ?? ({required onMerge}) async {},
|
||||
pendingChanges: pendingChanges,
|
||||
);
|
||||
if (syncOnce != null) {
|
||||
sync.syncNow();
|
||||
}
|
||||
final studysets = StudysetRepository(
|
||||
studysetGateway ?? FakeStudysetGateway(),
|
||||
);
|
||||
final cards = CardRepository(cardGateway ?? FakeCardGateway());
|
||||
final notes = NoteRepository(noteGateway ?? FakeNoteGateway());
|
||||
final paths = LearningPathRepository(
|
||||
pathGateway ?? FakeLearningPathGateway(),
|
||||
);
|
||||
final pathProgress = PathProgressRepository(
|
||||
answerGateway ?? FakePathAnswerGateway(),
|
||||
);
|
||||
final tags = TagRepository(
|
||||
tagGateway ?? FakeTagGateway(),
|
||||
cardChanges: cards.changes,
|
||||
noteChanges: notes.changes,
|
||||
pathChanges: paths.changes,
|
||||
);
|
||||
final settings = SettingsRepository(
|
||||
settingsGateway ?? FakeSettingsGateway(),
|
||||
);
|
||||
final reviews = ReviewRepository(reviewGateway ?? FakeReviewGateway());
|
||||
final assets = AssetRepository(
|
||||
assetGateway ?? FakeAssetGateway(),
|
||||
assetServer ?? FakeAssetServerGateway(),
|
||||
assetFiles ?? MemoryAssetFileCache(),
|
||||
);
|
||||
final pdfSources = PdfSourceRepository(
|
||||
pdfGateway ?? FakePdfSourceGateway(),
|
||||
assets,
|
||||
);
|
||||
final deletions = StudysetDeletions(
|
||||
studysets,
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
pdfSources,
|
||||
);
|
||||
final noteDeletions = NoteDeletions(notes);
|
||||
final cardDeletions = CardDeletions(cards);
|
||||
final pathDeletions = LearningPathDeletions(paths);
|
||||
final stepDeletions = StepDeletions(paths);
|
||||
final pdfDeletions = PdfDeletions(pdfSources);
|
||||
final studysetTabs = StudysetTabs(
|
||||
studysetTabStore ?? MemoryStudysetTabStore(),
|
||||
);
|
||||
await studysetTabs.open();
|
||||
return PersonDependencies(
|
||||
sync: sync,
|
||||
studysets: studysets,
|
||||
cards: cards,
|
||||
notes: notes,
|
||||
paths: paths,
|
||||
pathProgress: pathProgress,
|
||||
pdfSources: pdfSources,
|
||||
reviews: reviews,
|
||||
tags: tags,
|
||||
settings: settings,
|
||||
assets: assets,
|
||||
deletions: deletions,
|
||||
noteDeletions: noteDeletions,
|
||||
cardDeletions: cardDeletions,
|
||||
pathDeletions: pathDeletions,
|
||||
stepDeletions: stepDeletions,
|
||||
pdfDeletions: pdfDeletions,
|
||||
pomodoroRuns: pomodoroRuns ?? MemoryPomodoroRunStore(),
|
||||
studysetTabs: studysetTabs,
|
||||
// The picture as it is: drawing it anew is real work a widget
|
||||
// test's fake clock can't wait for (avatar_image_test.dart has it).
|
||||
profile: ProfileRepository(
|
||||
profileGateway ?? FakeProfileGateway(),
|
||||
drawAvatar: (picture) async => picture,
|
||||
),
|
||||
publishing: PublishRepository(
|
||||
publishGateway ?? FakePublishGateway(),
|
||||
publishedStore ?? MemoryPublishedStore(),
|
||||
),
|
||||
explore: ExploreRepository(
|
||||
exploreGateway ?? FakeExploreGateway(),
|
||||
ReleaseCopier(
|
||||
studysets,
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
pdfSources,
|
||||
tags,
|
||||
assets,
|
||||
),
|
||||
CopyUpdater(
|
||||
studysets,
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
pdfSources,
|
||||
tags,
|
||||
assets,
|
||||
),
|
||||
}) {
|
||||
final device = deviceDictionaries ?? noDeviceDictionaries();
|
||||
return Dependencies(
|
||||
serverName: 'learn.example.org',
|
||||
auth: AuthRepository(
|
||||
authGateway ?? FakeAuthGateway(),
|
||||
signInAddresses ?? MemorySignInAddresses(),
|
||||
),
|
||||
about: AboutRepository(aboutGateway ?? FakeAboutGateway()),
|
||||
dictionaries: DictionaryRepository(
|
||||
dictionaryGateway ?? FakeDictionaryGateway(),
|
||||
device,
|
||||
),
|
||||
deviceDictionaries: device,
|
||||
maps: maps ?? GeoMapRepository(),
|
||||
strokes: strokes ?? StrokeRepository(),
|
||||
sounds: sounds ?? FakeSoundPlayer(),
|
||||
files: files ?? FakeFilePicker(),
|
||||
fileSaver: fileSaver ?? FakeFileSaver(),
|
||||
videos: videos ?? FakeYoutubeVideos(),
|
||||
pdfEngine: pdfEngine ?? FakePdfEngine(),
|
||||
ankiPackages: InlineAnkiPackageReader(),
|
||||
archives: InlineArchiveReader(),
|
||||
exportArchiver: InlineExportArchiver(),
|
||||
deviceSettingsStore:
|
||||
deviceSettingsStore ?? MemoryDeviceSettingsStore(deviceSettings),
|
||||
deviceSettings: deviceSettings,
|
||||
appVersion: appVersion,
|
||||
pomodoro: pomodoro ?? FakePomodoroPlatform(),
|
||||
assistantHandOff:
|
||||
assistantHandOff ?? AssistantHandOff(open: (url) async => true),
|
||||
webSearch:
|
||||
webSearch ??
|
||||
WebSearch(
|
||||
open: (url) async => true,
|
||||
deviceSearch: (query) async => false,
|
||||
),
|
||||
assistant: AssistantRepository(assistantGateway ?? FakeAssistantGateway()),
|
||||
jellyfin: JellyfinRepository(
|
||||
(device) => jellyfinGateway ?? FakeJellyfinGateway(),
|
||||
jellyfinStore ?? MemoryJellyfinStore(),
|
||||
deviceName: 'Test',
|
||||
appVersion: appVersion.version,
|
||||
),
|
||||
jellyfinSuggestion:
|
||||
jellyfinSuggestion ?? JellyfinSuggestion(() async => null),
|
||||
mediaPlayers: mediaPlayers ?? FakeMediaPlayers(),
|
||||
openPerson:
|
||||
openPerson ??
|
||||
(person) async {
|
||||
final sync = SyncService(
|
||||
syncOnce ?? ({required onMerge}) async {},
|
||||
pendingChanges: pendingChanges,
|
||||
);
|
||||
if (syncOnce != null) {
|
||||
sync.syncNow();
|
||||
}
|
||||
final studysets = StudysetRepository(
|
||||
studysetGateway ?? FakeStudysetGateway(),
|
||||
);
|
||||
final cards = CardRepository(cardGateway ?? FakeCardGateway());
|
||||
final notes = NoteRepository(noteGateway ?? FakeNoteGateway());
|
||||
final paths = LearningPathRepository(
|
||||
pathGateway ?? FakeLearningPathGateway(),
|
||||
);
|
||||
final pathProgress = PathProgressRepository(
|
||||
answerGateway ?? FakePathAnswerGateway(),
|
||||
);
|
||||
final tags = TagRepository(
|
||||
tagGateway ?? FakeTagGateway(),
|
||||
cardChanges: cards.changes,
|
||||
noteChanges: notes.changes,
|
||||
pathChanges: paths.changes,
|
||||
);
|
||||
final settings = SettingsRepository(
|
||||
settingsGateway ?? FakeSettingsGateway(),
|
||||
);
|
||||
final reviews = ReviewRepository(
|
||||
reviewGateway ?? FakeReviewGateway(),
|
||||
);
|
||||
final assets = AssetRepository(
|
||||
assetGateway ?? FakeAssetGateway(),
|
||||
assetServer ?? FakeAssetServerGateway(),
|
||||
assetFiles ?? MemoryAssetFileCache(),
|
||||
);
|
||||
final pdfSources = PdfSourceRepository(
|
||||
pdfGateway ?? FakePdfSourceGateway(),
|
||||
assets,
|
||||
);
|
||||
final deletions = StudysetDeletions(
|
||||
studysets,
|
||||
),
|
||||
feedback: FeedbackRepository(
|
||||
feedbackWriteGateway ?? FakeFeedbackWriteGateway(),
|
||||
feedbackServerGateway ?? FakeFeedbackServerGateway(),
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
assets,
|
||||
),
|
||||
onDispose: () async {
|
||||
await deletions.dispose();
|
||||
await noteDeletions.dispose();
|
||||
await cardDeletions.dispose();
|
||||
await pathDeletions.dispose();
|
||||
await stepDeletions.dispose();
|
||||
await pdfDeletions.dispose();
|
||||
await studysets.dispose();
|
||||
await tags.dispose();
|
||||
await settings.dispose();
|
||||
await reviews.dispose();
|
||||
await cards.dispose();
|
||||
await notes.dispose();
|
||||
await paths.dispose();
|
||||
await pathProgress.dispose();
|
||||
await pdfSources.dispose();
|
||||
await assets.dispose();
|
||||
await sync.dispose();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
pdfSources,
|
||||
);
|
||||
final noteDeletions = NoteDeletions(notes);
|
||||
final cardDeletions = CardDeletions(cards);
|
||||
final pathDeletions = LearningPathDeletions(paths);
|
||||
final stepDeletions = StepDeletions(paths);
|
||||
final pdfDeletions = PdfDeletions(pdfSources);
|
||||
final studysetTabs = StudysetTabs(
|
||||
studysetTabStore ?? MemoryStudysetTabStore(),
|
||||
);
|
||||
await studysetTabs.open();
|
||||
return PersonDependencies(
|
||||
sync: sync,
|
||||
studysets: studysets,
|
||||
cards: cards,
|
||||
notes: notes,
|
||||
paths: paths,
|
||||
pathProgress: pathProgress,
|
||||
pdfSources: pdfSources,
|
||||
reviews: reviews,
|
||||
tags: tags,
|
||||
settings: settings,
|
||||
assets: assets,
|
||||
deletions: deletions,
|
||||
noteDeletions: noteDeletions,
|
||||
cardDeletions: cardDeletions,
|
||||
pathDeletions: pathDeletions,
|
||||
stepDeletions: stepDeletions,
|
||||
pdfDeletions: pdfDeletions,
|
||||
pomodoroRuns: pomodoroRuns ?? MemoryPomodoroRunStore(),
|
||||
studysetTabs: studysetTabs,
|
||||
// The picture as it is: drawing it anew is real work a widget
|
||||
// test's fake clock can't wait for (avatar_image_test.dart has it).
|
||||
profile: ProfileRepository(
|
||||
profileGateway ?? FakeProfileGateway(),
|
||||
drawAvatar: (picture) async => picture,
|
||||
),
|
||||
publishing: PublishRepository(
|
||||
publishGateway ?? FakePublishGateway(),
|
||||
publishedStore ?? MemoryPublishedStore(),
|
||||
),
|
||||
explore: ExploreRepository(
|
||||
exploreGateway ?? FakeExploreGateway(),
|
||||
ReleaseCopier(
|
||||
studysets,
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
pdfSources,
|
||||
tags,
|
||||
assets,
|
||||
),
|
||||
CopyUpdater(
|
||||
studysets,
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
pdfSources,
|
||||
tags,
|
||||
assets,
|
||||
),
|
||||
studysets,
|
||||
),
|
||||
feedback: FeedbackRepository(
|
||||
feedbackWriteGateway ?? FakeFeedbackWriteGateway(),
|
||||
feedbackServerGateway ?? FakeFeedbackServerGateway(),
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
assets,
|
||||
),
|
||||
onDispose: () async {
|
||||
await deletions.dispose();
|
||||
await noteDeletions.dispose();
|
||||
await cardDeletions.dispose();
|
||||
await pathDeletions.dispose();
|
||||
await stepDeletions.dispose();
|
||||
await pdfDeletions.dispose();
|
||||
await studysets.dispose();
|
||||
await tags.dispose();
|
||||
await settings.dispose();
|
||||
await reviews.dispose();
|
||||
await cards.dispose();
|
||||
await notes.dispose();
|
||||
await paths.dispose();
|
||||
await pathProgress.dispose();
|
||||
await pdfSources.dispose();
|
||||
await assets.dispose();
|
||||
await sync.dispose();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Reads Anki packages where it is asked to: a widget test's fake clock
|
||||
/// can't wait for another isolate.
|
||||
|
|
|
|||
Loading…
Reference in a new issue