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

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