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

@ -2005,6 +2005,560 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get studyButtonGone => 'That studyset no longer exists.';
@override
String get pdfPageField => 'PDF page';
@override
String get studysetTabFiles => 'Files';
@override
String get pdfUploadMenu => 'Upload a PDF';
@override
String get pdfFileType => 'PDFs';
@override
String pdfAdded(String title) {
return 'Added \"$title\".';
}
@override
String get pdfNotPdf => 'This file isn\'t a PDF.';
@override
String pdfTooLarge(int megabytes) {
return 'This PDF is larger than $megabytes MB.';
}
@override
String get pdfDuplicateTitle => 'PDF already here';
@override
String pdfDuplicateMessage(String title) {
return 'This file is \"$title\", which this studyset holds already. Add it as a copy, or keep the one there?';
}
@override
String get pdfDuplicateKeep => 'Keep the one there';
@override
String get pdfDuplicateAdd => 'Add a copy';
@override
String get filesEmpty => 'No files yet';
@override
String get filesEmptyHint =>
'Use \"Add material → Upload a PDF\" to add one.';
@override
String get filesSort => 'Sort';
@override
String get filesSortName => 'Name (A–Z)';
@override
String get filesSortNewest => 'Newest first';
@override
String get filesSortOldest => 'Oldest first';
@override
String get filesSortLargest => 'Largest first';
@override
String pdfPages(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count pages',
one: '1 page',
);
return '$_temp0';
}
@override
String pdfPercentRead(int percent) {
return '$percent% read';
}
@override
String get pdfDeadlineMet => 'Reading deadline met';
@override
String pdfDeadlineUpcoming(int days, int pages) {
String _temp0 = intl.Intl.pluralLogic(
days,
locale: localeName,
other: '$days days',
one: '1 day',
);
return 'Starts in $_temp0 — $pages/day planned';
}
@override
String pdfDeadlinePassed(int pages) {
String _temp0 = intl.Intl.pluralLogic(
pages,
locale: localeName,
other: '$pages pages',
one: '1 page',
);
return 'Deadline passed — $_temp0 left';
}
@override
String pdfDeadlineToday(int pages) {
String _temp0 = intl.Intl.pluralLogic(
pages,
locale: localeName,
other: '$pages pages',
one: '1 page',
);
return 'Due today — $_temp0 left';
}
@override
String pdfDeadlinePace(int pages, int target, int days) {
String _temp0 = intl.Intl.pluralLogic(
days,
locale: localeName,
other: '$days days',
one: '1 day',
);
return '$pages/day to reach page $target in $_temp0';
}
@override
String get pdfOptions => 'Actions';
@override
String get pdfRename => 'Rename';
@override
String get pdfRenameTitle => 'Rename PDF';
@override
String get pdfRenameLabel => 'Title';
@override
String get pdfSetDeadline => 'Set reading deadline';
@override
String get pdfEditDeadline => 'Edit reading deadline';
@override
String get pdfMarkFinished => 'Mark as finished';
@override
String get pdfResetReading => 'Reset reading progress';
@override
String get pdfResetTitle => 'Reset reading progress?';
@override
String pdfResetMessage(String title) {
return 'How far you\'ve read \"$title\" is forgotten. The PDF, its flashcards and notes stay.';
}
@override
String get pdfResetConfirm => 'Reset';
@override
String get pdfMoreInfo => 'More info';
@override
String get pdfInfoFileName => 'File name';
@override
String get pdfInfoSize => 'Size';
@override
String get pdfInfoPages => 'Pages';
@override
String get pdfInfoUnknown => 'Known once it is opened';
@override
String get pdfDelete => 'Delete';
@override
String get pdfDeleteTitle => 'Delete PDF?';
@override
String pdfDeleteMessage(String title) {
return '\"$title\" is removed from this studyset, with how far it was read. Flashcards and notes made from it stay.';
}
@override
String get pdfDeleteConfirm => 'Delete';
@override
String get pdfDeleted => 'PDF deleted';
@override
String get deadlineSheetSet => 'Set a reading deadline';
@override
String get deadlineSheetEdit => 'Edit reading deadline';
@override
String get deadlineSheetHelp =>
'Reach a page by a day. Progress follows the furthest page you reach as you read.';
@override
String deadlineReachPageOf(int count) {
return 'Reach page (of $count)';
}
@override
String get deadlineReachPage => 'Reach page';
@override
String get deadlineDecrease => 'Decrease';
@override
String get deadlineIncrease => 'Increase';
@override
String get deadlineChapter => 'Or finish a chapter';
@override
String deadlineChapterEntry(String title, int page) {
return '$title · to p.$page';
}
@override
String get deadlineBy => 'By';
@override
String get deadlineStart => 'Start on';
@override
String get deadlineStartToday => 'Today';
@override
String get deadlineStartReset => 'Start today';
@override
String get deadlineStartHelp =>
'The daily pace counts from this day on. Until then the PDF waits under \"Upcoming\".';
@override
String get deadlineSave => 'Save deadline';
@override
String get deadlineRemove => 'Remove deadline';
@override
String get readerMissing =>
'This PDF isn\'t on this device yet. It opens once the device that added it has uploaded it and you\'re online.';
@override
String get readerUnreadable => 'This file can\'t be read as a PDF.';
@override
String get readerBack => 'Back to studyset';
@override
String get readerSearch => 'Search PDF';
@override
String get readerFitWidth => 'Fit width';
@override
String get readerFitHeight => 'Fit height';
@override
String get readerZoomIn => 'Zoom in';
@override
String get readerZoomOut => 'Zoom out';
@override
String get readerSetZoom => 'Set zoom';
@override
String get readerZoomTitle => 'Zoom';
@override
String get readerZoomApply => 'Apply';
@override
String get readerFullscreen => 'Fullscreen';
@override
String get readerExitFullscreen => 'Exit fullscreen';
@override
String get readerPreviousPage => 'Previous page';
@override
String get readerNextPage => 'Next page';
@override
String readerPageOf(int count) {
return '/ $count';
}
@override
String get readerFindHint => 'Find in PDF';
@override
String readerMatchOf(int current, int total) {
return '$current of $total';
}
@override
String get readerPreviousMatch => 'Previous match';
@override
String get readerNextMatch => 'Next match';
@override
String get readerCloseSearch => 'Close search';
@override
String get readerExpand => 'Expand';
@override
String get readerCollapse => 'Collapse';
@override
String get readerTabFlashcards => 'Flashcards';
@override
String get readerTabNotes => 'Notes';
@override
String get readerCardCreated => 'Flashcard created';
@override
String get readerCardDeleteTitle => 'Delete flashcard?';
@override
String get readerCardDeleteMessage =>
'The flashcard is deleted, with how it was studied.';
@override
String get readerCardDeleteConfirm => 'Delete';
@override
String get readerFrontMatter => 'Front matter';
@override
String readerPage(int page) {
return 'Page $page';
}
@override
String get readerByPage => 'Page';
@override
String get readerByChapter => 'Chapter';
@override
String get readerCardsEmpty => 'Select text to make flashcards in no time';
@override
String get readerCardsEmptyHint =>
'Select the lines that matter and send them to a question or an answer, or press \"Create flashcard\".';
@override
String get readerCreateCard => 'Create flashcard';
@override
String get readerEditCard => 'Edit flashcard';
@override
String get readerBackToCards => 'Back to flashcards';
@override
String readerReferencedPage(int page) {
return 'Referenced page: $page';
}
@override
String readerUseCurrentPage(int page) {
return 'Use current ($page)';
}
@override
String get readerQuestion => 'Question';
@override
String get readerAnswer => 'Answer';
@override
String get readerKeepOpen => 'Keep creation mode open';
@override
String get readerClear => 'Clear';
@override
String get readerCreate => 'Create';
@override
String get readerUntitledCard => 'Untitled flashcard';
@override
String get readerCardOptions => 'Card options';
@override
String get readerNoteSaved => 'Saved';
@override
String get readerNoteEditing => 'Editing…';
@override
String get readerNoteSaving => 'Saving…';
@override
String get readerNoteFailed => 'Save failed';
@override
String get readerToQuestion => 'Question';
@override
String get readerToAnswer => 'Answer';
@override
String get readerToNote => 'Notes';
@override
String get readerLineBreaksJoined => 'Line breaks joined (tap to keep)';
@override
String get readerLineBreaksKept => 'Line breaks kept (tap to join)';
@override
String get blockScreenshot => 'Screenshot of the page';
@override
String get readerCaptureCancel => 'Cancel screenshot';
@override
String get readerCaptureHint => 'Drag over the part of the page to keep';
@override
String get readerCaptureFailed =>
'The page couldn\'t be drawn. Try again once it has loaded.';
@override
String get readerScreenshotSaved => 'Screenshot saved';
@override
String get imageOpenPdfPage => 'Open the PDF at this page';
@override
String get homeWidgetDeadlines => 'Reading deadlines';
@override
String get deadlinesTitle => 'Reading deadlines';
@override
String get deadlinesCurrent => 'Current';
@override
String get deadlinesUpcoming => 'Upcoming';
@override
String deadlinesProgress(int furthest, int target, String when) {
return 'p.$furthest/$target · $when';
}
@override
String get deadlinePassedShort => 'deadline passed';
@override
String get deadlineDueTodayShort => 'due today';
@override
String deadlineDaysLeft(int days) {
String _temp0 = intl.Intl.pluralLogic(
days,
locale: localeName,
other: '$days days left',
one: '1 day left',
);
return '$_temp0';
}
@override
String deadlineToPageToday(int page) {
return 'to p.$page today';
}
@override
String deadlineStartsIn(int days) {
String _temp0 = intl.Intl.pluralLogic(
days,
locale: localeName,
other: '$days days',
one: '1 day',
);
return 'Starts in $_temp0';
}
@override
String get deadlinesPlaceholder => 'Appears when you have reading deadlines';
@override
String get deadlinesOpenTimeline => 'Open the timeline';
@override
String get timelineTitle => 'Reading timeline';
@override
String get timelineEmpty => 'No reading deadlines yet';
@override
String get timelineEmptyHint =>
'Set a reading deadline on a PDF to see it here.';
@override
String get timelineToday => 'Today';
@override
String timelinePages(int furthest, int target) {
return 'p.$furthest/$target';
}
@override
String chapterFinished(String chapter, int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count cards',
one: 'card',
);
return 'Finished “$chapter” — study the $_temp0 you made?';
}
@override
String get chapterStudyNow => 'Study now';
@override
String get chapterDismiss => 'Dismiss';
@override
String get deadlineChapterLoading => 'Loading chapters…';
@override
String fileSizeKb(int size) {
return '$size KB';
}
@override
String fileSizeMb(String size) {
return '$size MB';
}
@override
String pdfPageShort(int page) {
return 'p. $page';
}
@override
String get ankiImportMenu => 'Import from Anki';