Questions asked from the app went to Claude alone. Settings › Assistant
now chooses where they open (decided 2026-10-14): Lumo, the default, by
its guest chat with the question in the address's fragment (Lumo's
`prefill`, which needs no account and reaches no server; the address for
one's own account loses the question when nobody is signed in), ChatGPT,
Claude, Mistral, Perplexity, Duck.ai, Grok, Kagi Assistant, HuggingChat,
or an address of one's own with %s. Each link fills the question in for
the person to send (Perplexity answers at once, Grok asks first); Gemini,
which has no such link, and Copilot, whose stopped working, are left out.
The field names the assistant ("Ask Lumo…"). A device that sent questions
to Claude goes on asking an assistant, now Lumo. Connecting Claude to read
studysets stays as it was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
369 lines
17 KiB
Dart
369 lines
17 KiB
Dart
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_file_cache.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_server_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/asset/data/sound_player.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/files/file_picker.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/files/file_saver.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/sync/pending_changes_store.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/video/media_players.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/video/youtube_videos.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/about/data/about_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/about/data/about_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package_reader.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/archive/model/import_archive.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_hand_off.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/assistant/data/web_search.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_repository.dart';
|
|
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/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';
|
|
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_copier.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/export/data/export_archive.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/export/data/studyset_markdown.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_store.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_suggestion.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/kanji/data/stroke_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_answer_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/note/data/note_deletions.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/note/data/note_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_deletions.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_engine.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_run_store.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/publish/data/published_store.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
|
|
|
import 'fake_about_gateway.dart';
|
|
import 'fake_asset_gateway.dart';
|
|
import 'fake_asset_server_gateway.dart';
|
|
import 'fake_assistant_gateway.dart';
|
|
import 'fake_auth_gateway.dart';
|
|
import 'fake_card_gateway.dart';
|
|
import 'fake_dictionary_gateway.dart';
|
|
import 'fake_explore_gateway.dart';
|
|
import 'fake_feedback_gateways.dart';
|
|
import 'fake_file_picker.dart';
|
|
import 'fake_file_saver.dart';
|
|
import 'fake_jellyfin.dart';
|
|
import 'fake_learning_path_gateway.dart';
|
|
import 'fake_note_gateway.dart';
|
|
import 'fake_path_answer_gateway.dart';
|
|
import 'fake_pdf_engine.dart';
|
|
import 'fake_pdf_source_gateway.dart';
|
|
import 'fake_pomodoro_platform.dart';
|
|
import 'fake_profile_gateway.dart';
|
|
import 'fake_publish_gateway.dart';
|
|
import 'fake_review_gateway.dart';
|
|
import 'fake_settings_gateway.dart';
|
|
import 'fake_sound_player.dart';
|
|
import 'fake_studyset_gateway.dart';
|
|
import 'fake_tag_gateway.dart';
|
|
import 'fake_youtube_videos.dart';
|
|
import 'memory_asset_file_cache.dart';
|
|
import 'memory_device_settings_store.dart';
|
|
import 'memory_pomodoro_run_store.dart';
|
|
import 'memory_published_store.dart';
|
|
import 'memory_studyset_tab_store.dart';
|
|
|
|
/// The app's dependencies with fakes instead of the server and the device's
|
|
/// database; each test passes in only the fakes it looks at.
|
|
///
|
|
/// Every person gets [studysetGateway], [cardGateway], [noteGateway],
|
|
/// [pathGateway], [answerGateway], [tagGateway], [settingsGateway] and the
|
|
/// asset fakes (or fresh fakes of their own),
|
|
/// unless [openPerson] replaces how a person's dependencies are opened.
|
|
///
|
|
/// The device starts with [deviceSettings], kept in memory unless a test
|
|
/// passes the [deviceSettingsStore] to look at.
|
|
///
|
|
/// A person's sync runs [syncOnce] (by default, going through without doing
|
|
/// anything): once when their data opens if a test passes it, and whenever
|
|
/// the person asks; never on timers, which a widget test would find pending.
|
|
Dependencies testDependencies({
|
|
AuthGateway? authGateway,
|
|
StudysetGateway? studysetGateway,
|
|
CardGateway? cardGateway,
|
|
NoteGateway? noteGateway,
|
|
LearningPathGateway? pathGateway,
|
|
PathAnswerGateway? answerGateway,
|
|
PdfSourceGateway? pdfGateway,
|
|
ReviewGateway? reviewGateway,
|
|
TagGateway? tagGateway,
|
|
GeoMapRepository? maps,
|
|
StrokeRepository? strokes,
|
|
SoundPlayer? sounds,
|
|
FilePicker? files,
|
|
FileSaver? fileSaver,
|
|
YoutubeVideos? videos,
|
|
PdfEngine? pdfEngine,
|
|
SettingsGateway? settingsGateway,
|
|
AssetGateway? assetGateway,
|
|
AssetServerGateway? assetServer,
|
|
AssetFileCache? assetFiles,
|
|
AboutGateway? aboutGateway,
|
|
DictionaryGateway? dictionaryGateway,
|
|
DeviceSettingsStore? deviceSettingsStore,
|
|
DeviceSettings deviceSettings = const DeviceSettings(),
|
|
AppVersion appVersion = const AppVersion(version: '1.0.0', build: '1'),
|
|
PomodoroPlatform? pomodoro,
|
|
PomodoroRunStore? pomodoroRuns,
|
|
ProfileGateway? profileGateway,
|
|
PublishGateway? publishGateway,
|
|
PublishedStore? publishedStore,
|
|
StudysetTabStore? studysetTabStore,
|
|
ExploreGateway? exploreGateway,
|
|
FeedbackWriteGateway? feedbackWriteGateway,
|
|
FeedbackServerGateway? feedbackServerGateway,
|
|
SyncOnce? syncOnce,
|
|
PendingChangesStore? pendingChanges,
|
|
OpenPerson? openPerson,
|
|
AssistantHandOff? assistantHandOff,
|
|
WebSearch? webSearch,
|
|
AssistantGateway? assistantGateway,
|
|
JellyfinGateway? jellyfinGateway,
|
|
JellyfinStore? jellyfinStore,
|
|
JellyfinSuggestion? jellyfinSuggestion,
|
|
MediaPlayers? mediaPlayers,
|
|
}) => Dependencies(
|
|
serverName: 'learn.example.org',
|
|
auth: AuthRepository(authGateway ?? FakeAuthGateway()),
|
|
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,
|
|
),
|
|
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.
|
|
class InlineAnkiPackageReader() implements AnkiPackageReader {
|
|
@override
|
|
Future<AnkiPackage> read(Uint8List bytes) async =>
|
|
readAnkiPackage(bytes, temp: Directory.systemTemp);
|
|
}
|
|
|
|
/// Reads archives where it is asked to, as [InlineAnkiPackageReader] reads.
|
|
class InlineArchiveReader() implements ArchiveReader {
|
|
@override
|
|
Future<ImportArchive> read(Uint8List bytes) async => readArchive(bytes);
|
|
}
|
|
|
|
/// Packs exports where it is asked to, as [InlineAnkiPackageReader] reads.
|
|
class InlineExportArchiver() implements ExportArchiver {
|
|
@override
|
|
Future<Uint8List> zip(List<ExportFile> files) async => zipExport(files);
|
|
}
|