feat: PDF sources: read, take notes and cards from pages, deadlines
Some checks failed
CI / check (push) Failing after 26s
CI / deploy (push) Has been skipped

PDFs live in a studyset's new Files tab: uploaded from Add material
(a file the studyset holds already asks first), each row with its
pages, cards, notes and size, how far it is read and its reading
deadline; renamed, given a deadline (a page or a chapter by a day),
marked as read, started afresh, deleted with a moment to undo.

The reader (/pdfs/:id) opens where reading left off: on wide windows
a rail of tools (search, fit, zoom, fullscreen), a page counter with
the chapter, and a sidebar; on phones a top bar, a page scrubber and
a sheet. The sidebar holds the PDF's flashcards, by page or chapter,
made from selected text or written there, each tied to its page and
tagged with its chapter; and the page's note, an ordinary note tied
to the page. Screenshots of a region become images that know their
page. Cards and screenshots lead back to their page; reading on past
a chapter with cards suggests studying it. Home shows the deadlines
running and to come, and a timeline of them.

Server: pdf_source rows (content in the payload, last writer wins)
and an append-only pdf_reading log, the furthest page derived from
it so devices never conflict. Files up to 50 MB for PDFs (20 MB for
others; a larger file that isn't a PDF is deleted once stored). A
deleted PDF takes its file's asset row along unless another PDF has
the same file. Card payload 4 and note payload 2 keep the page.

The reader draws with pdfrx (PDFium) behind PdfEngine in
Dependencies; widget tests use a fake, and
integration_test/pdf_engine_test.dart checks the real engine on a
device or the desktop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-03 14:40:08 +02:00
commit b96db731c1
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -27,6 +27,10 @@ import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progre
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/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';
@ -46,6 +50,8 @@ import 'fake_file_picker.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_review_gateway.dart';
import 'fake_settings_gateway.dart';
import 'fake_sound_player.dart';
@ -68,12 +74,14 @@ Dependencies testDependencies({
NoteGateway? noteGateway,
LearningPathGateway? pathGateway,
PathAnswerGateway? answerGateway,
PdfSourceGateway? pdfGateway,
ReviewGateway? reviewGateway,
TagGateway? tagGateway,
GeoMapRepository? maps,
SoundPlayer? sounds,
FilePicker? files,
YoutubeVideos? videos,
PdfEngine? pdfEngine,
SettingsGateway? settingsGateway,
AssetGateway? assetGateway,
AssetServerGateway? assetServer,
@ -87,6 +95,7 @@ Dependencies testDependencies({
sounds: sounds ?? FakeSoundPlayer(),
files: files ?? FakeFilePicker(),
videos: videos ?? FakeYoutubeVideos(),
pdfEngine: pdfEngine ?? FakePdfEngine(),
ankiPackages: InlineAnkiPackageReader(),
openPerson:
openPerson ??
@ -112,21 +121,33 @@ Dependencies testDependencies({
settingsGateway ?? FakeSettingsGateway(),
);
final reviews = ReviewRepository(reviewGateway ?? FakeReviewGateway());
final deletions = StudysetDeletions(studysets, cards, notes, paths);
final noteDeletions = NoteDeletions(notes);
final pathDeletions = LearningPathDeletions(paths);
final stepDeletions = StepDeletions(paths);
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 pathDeletions = LearningPathDeletions(paths);
final stepDeletions = StepDeletions(paths);
final pdfDeletions = PdfDeletions(pdfSources);
return PersonDependencies(
studysets: studysets,
cards: cards,
notes: notes,
paths: paths,
pathProgress: pathProgress,
pdfSources: pdfSources,
reviews: reviews,
tags: tags,
settings: settings,
@ -135,11 +156,13 @@ Dependencies testDependencies({
noteDeletions: noteDeletions,
pathDeletions: pathDeletions,
stepDeletions: stepDeletions,
pdfDeletions: pdfDeletions,
onDispose: () async {
await deletions.dispose();
await noteDeletions.dispose();
await pathDeletions.dispose();
await stepDeletions.dispose();
await pdfDeletions.dispose();
await studysets.dispose();
await tags.dispose();
await settings.dispose();
@ -148,6 +171,7 @@ Dependencies testDependencies({
await notes.dispose();
await paths.dispose();
await pathProgress.dispose();
await pdfSources.dispose();
await assets.dispose();
},
);