feat: PDF sources: read, take notes and cards from pages, deadlines
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:
parent
ec34bb2334
commit
b96db731c1
72 changed files with 34926 additions and 58 deletions
135
halkyon_learn_flutter/integration_test/pdf_engine_test.dart
Normal file
135
halkyon_learn_flutter/integration_test/pdf_engine_test.dart
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_engine.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdfrx_engine.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
// The app's PDF engine for real, PDFium included, which `flutter test`
|
||||
// can't load: on a device or the desktop,
|
||||
//
|
||||
// flutter test integration_test/pdf_engine_test.dart -d linux
|
||||
//
|
||||
// on a PDF of three pages, "Hello page N" on each, with an outline of two
|
||||
// chapters.
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(PdfrxEngine.initialize);
|
||||
|
||||
testWidgets('opens a PDF, reads its chapters and draws a region', (
|
||||
tester,
|
||||
) async {
|
||||
final document = await tester.runAsync(
|
||||
() => PdfrxEngine().open(_threePages()),
|
||||
);
|
||||
addTearDown(() => tester.runAsync(document!.dispose));
|
||||
|
||||
expect(document!.pageCount, 3);
|
||||
expect(await tester.runAsync(document.chapters), [
|
||||
(title: 'One', page: 1),
|
||||
(title: 'Two', page: 3),
|
||||
]);
|
||||
final png = await tester.runAsync(
|
||||
() => document.renderRegion(1, const Rect.fromLTWH(0, 0, 0.5, 0.5)),
|
||||
);
|
||||
expect(png!.sublist(0, 4), [0x89, 0x50, 0x4e, 0x47]);
|
||||
});
|
||||
|
||||
testWidgets('refuses a file that is no PDF', (tester) async {
|
||||
// runAsync reports what is thrown inside instead of passing it on.
|
||||
final refused = await tester.runAsync(() async {
|
||||
try {
|
||||
await PdfrxEngine().open(Uint8List.fromList(utf8.encode('text')));
|
||||
return false;
|
||||
} on FormatException {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
expect(refused, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('its viewer turns pages and finds text', (tester) async {
|
||||
final document = await tester.runAsync(
|
||||
() => PdfrxEngine().open(_threePages()),
|
||||
);
|
||||
final controller = PdfReaderController();
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 600,
|
||||
height: 800,
|
||||
child: document!.viewer(controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
Future<void> until(bool Function() test) async {
|
||||
final waited = Stopwatch()..start();
|
||||
while (!test() && waited.elapsed < const Duration(seconds: 20)) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
await until(() => controller.isReady);
|
||||
expect(controller.isReady, isTrue);
|
||||
|
||||
// The page turns over an animation, whose frames the test drives: it
|
||||
// is started, then pumped until it arrives.
|
||||
unawaited(controller.goToPage(3));
|
||||
await until(() => controller.page == 3);
|
||||
expect(controller.page, 3);
|
||||
|
||||
controller.search('hello');
|
||||
await until(() => controller.matchCount == 3);
|
||||
expect(controller.matchCount, 3);
|
||||
expect(controller.pageRect(3), isNotNull);
|
||||
});
|
||||
}
|
||||
|
||||
/// A PDF of three pages, each saying "Hello page N", with an outline of
|
||||
/// chapter "One" at page 1 and "Two" at page 3.
|
||||
Uint8List _threePages() {
|
||||
String text(int page) {
|
||||
final stream = 'BT /F1 24 Tf 40 700 Td (Hello page $page) Tj ET';
|
||||
return '<< /Length ${stream.length} >>\nstream\n$stream\nendstream';
|
||||
}
|
||||
|
||||
String pageOf(int page) =>
|
||||
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] '
|
||||
'/Contents ${5 + page} 0 R /Resources << /Font << /F1 9 0 R >> >> >>';
|
||||
|
||||
final objects = [
|
||||
'<< /Type /Catalog /Pages 2 0 R /Outlines 10 0 R >>',
|
||||
'<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>',
|
||||
for (final page in [1, 2, 3]) pageOf(page),
|
||||
for (final page in [1, 2, 3]) text(page),
|
||||
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
|
||||
'<< /Type /Outlines /First 11 0 R /Last 12 0 R /Count 2 >>',
|
||||
'<< /Title (One) /Parent 10 0 R /Next 12 0 R /Dest [3 0 R /Fit] >>',
|
||||
'<< /Title (Two) /Parent 10 0 R /Prev 11 0 R /Dest [5 0 R /Fit] >>',
|
||||
];
|
||||
final out = StringBuffer('%PDF-1.7\n');
|
||||
final offsets = <int>[];
|
||||
for (final (index, object) in objects.indexed) {
|
||||
offsets.add(out.length);
|
||||
out.write('${index + 1} 0 obj\n$object\nendobj\n');
|
||||
}
|
||||
final xref = out.length;
|
||||
out
|
||||
..write('xref\n0 ${objects.length + 1}\n0000000000 65535 f \n')
|
||||
..writeAll([
|
||||
for (final offset in offsets)
|
||||
'${offset.toString().padLeft(10, '0')} 00000 n \n',
|
||||
])
|
||||
..write(
|
||||
'trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\n'
|
||||
'startxref\n$xref\n%%EOF\n',
|
||||
);
|
||||
return Uint8List.fromList(latin1.encode(out.toString()));
|
||||
}
|
||||
|
|
@ -655,6 +655,185 @@
|
|||
"studyButtonRemove": "Remove study button",
|
||||
"studyButtonBroken": "This study button doesn't say what to study.",
|
||||
"studyButtonGone": "That studyset no longer exists.",
|
||||
"pdfPageField": "PDF page",
|
||||
"studysetTabFiles": "Files",
|
||||
"pdfUploadMenu": "Upload a PDF",
|
||||
"pdfFileType": "PDFs",
|
||||
"pdfAdded": "Added \"{title}\".",
|
||||
"@pdfAdded": {"placeholders": {"title": {"type": "String"}}},
|
||||
"pdfNotPdf": "This file isn't a PDF.",
|
||||
"pdfTooLarge": "This PDF is larger than {megabytes} MB.",
|
||||
"@pdfTooLarge": {"placeholders": {"megabytes": {"type": "int"}}},
|
||||
"pdfDuplicateTitle": "PDF already here",
|
||||
"pdfDuplicateMessage": "This file is \"{title}\", which this studyset holds already. Add it as a copy, or keep the one there?",
|
||||
"@pdfDuplicateMessage": {"placeholders": {"title": {"type": "String"}}},
|
||||
"pdfDuplicateKeep": "Keep the one there",
|
||||
"pdfDuplicateAdd": "Add a copy",
|
||||
"filesEmpty": "No files yet",
|
||||
"filesEmptyHint": "Use \"Add material → Upload a PDF\" to add one.",
|
||||
"filesSort": "Sort",
|
||||
"filesSortName": "Name (A–Z)",
|
||||
"filesSortNewest": "Newest first",
|
||||
"filesSortOldest": "Oldest first",
|
||||
"filesSortLargest": "Largest first",
|
||||
"pdfPages": "{count, plural, =1{1 page} other{{count} pages}}",
|
||||
"@pdfPages": {"placeholders": {"count": {"type": "int"}}},
|
||||
"pdfPercentRead": "{percent}% read",
|
||||
"@pdfPercentRead": {"placeholders": {"percent": {"type": "int"}}},
|
||||
"pdfDeadlineMet": "Reading deadline met",
|
||||
"pdfDeadlineUpcoming": "Starts in {days, plural, =1{1 day} other{{days} days}} — {pages}/day planned",
|
||||
"@pdfDeadlineUpcoming": {"placeholders": {"days": {"type": "int"}, "pages": {"type": "int"}}},
|
||||
"pdfDeadlinePassed": "Deadline passed — {pages, plural, =1{1 page} other{{pages} pages}} left",
|
||||
"@pdfDeadlinePassed": {"placeholders": {"pages": {"type": "int"}}},
|
||||
"pdfDeadlineToday": "Due today — {pages, plural, =1{1 page} other{{pages} pages}} left",
|
||||
"@pdfDeadlineToday": {"placeholders": {"pages": {"type": "int"}}},
|
||||
"pdfDeadlinePace": "{pages}/day to reach page {target} in {days, plural, =1{1 day} other{{days} days}}",
|
||||
"@pdfDeadlinePace": {"placeholders": {"pages": {"type": "int"}, "target": {"type": "int"}, "days": {"type": "int"}}},
|
||||
"pdfOptions": "Actions",
|
||||
"pdfRename": "Rename",
|
||||
"pdfRenameTitle": "Rename PDF",
|
||||
"pdfRenameLabel": "Title",
|
||||
"pdfSetDeadline": "Set reading deadline",
|
||||
"pdfEditDeadline": "Edit reading deadline",
|
||||
"pdfMarkFinished": "Mark as finished",
|
||||
"pdfResetReading": "Reset reading progress",
|
||||
"pdfResetTitle": "Reset reading progress?",
|
||||
"pdfResetMessage": "How far you've read \"{title}\" is forgotten. The PDF, its flashcards and notes stay.",
|
||||
"@pdfResetMessage": {"placeholders": {"title": {"type": "String"}}},
|
||||
"pdfResetConfirm": "Reset",
|
||||
"pdfMoreInfo": "More info",
|
||||
"pdfInfoFileName": "File name",
|
||||
"pdfInfoSize": "Size",
|
||||
"pdfInfoPages": "Pages",
|
||||
"pdfInfoUnknown": "Known once it is opened",
|
||||
"pdfDelete": "Delete",
|
||||
"pdfDeleteTitle": "Delete PDF?",
|
||||
"pdfDeleteMessage": "\"{title}\" is removed from this studyset, with how far it was read. Flashcards and notes made from it stay.",
|
||||
"@pdfDeleteMessage": {"placeholders": {"title": {"type": "String"}}},
|
||||
"pdfDeleteConfirm": "Delete",
|
||||
"pdfDeleted": "PDF deleted",
|
||||
"deadlineSheetSet": "Set a reading deadline",
|
||||
"deadlineSheetEdit": "Edit reading deadline",
|
||||
"deadlineSheetHelp": "Reach a page by a day. Progress follows the furthest page you reach as you read.",
|
||||
"deadlineReachPageOf": "Reach page (of {count})",
|
||||
"@deadlineReachPageOf": {"placeholders": {"count": {"type": "int"}}},
|
||||
"deadlineReachPage": "Reach page",
|
||||
"deadlineDecrease": "Decrease",
|
||||
"deadlineIncrease": "Increase",
|
||||
"deadlineChapter": "Or finish a chapter",
|
||||
"deadlineChapterEntry": "{title} · to p.{page}",
|
||||
"@deadlineChapterEntry": {"placeholders": {"title": {"type": "String"}, "page": {"type": "int"}}},
|
||||
"deadlineBy": "By",
|
||||
"deadlineStart": "Start on",
|
||||
"deadlineStartToday": "Today",
|
||||
"deadlineStartReset": "Start today",
|
||||
"deadlineStartHelp": "The daily pace counts from this day on. Until then the PDF waits under \"Upcoming\".",
|
||||
"deadlineSave": "Save deadline",
|
||||
"deadlineRemove": "Remove deadline",
|
||||
"readerMissing": "This PDF isn't on this device yet. It opens once the device that added it has uploaded it and you're online.",
|
||||
"readerUnreadable": "This file can't be read as a PDF.",
|
||||
"readerBack": "Back to studyset",
|
||||
"readerSearch": "Search PDF",
|
||||
"readerFitWidth": "Fit width",
|
||||
"readerFitHeight": "Fit height",
|
||||
"readerZoomIn": "Zoom in",
|
||||
"readerZoomOut": "Zoom out",
|
||||
"readerSetZoom": "Set zoom",
|
||||
"readerZoomTitle": "Zoom",
|
||||
"readerZoomApply": "Apply",
|
||||
"readerFullscreen": "Fullscreen",
|
||||
"readerExitFullscreen": "Exit fullscreen",
|
||||
"readerPreviousPage": "Previous page",
|
||||
"readerNextPage": "Next page",
|
||||
"readerPageOf": "/ {count}",
|
||||
"@readerPageOf": {"placeholders": {"count": {"type": "int"}}},
|
||||
"readerFindHint": "Find in PDF",
|
||||
"readerMatchOf": "{current} of {total}",
|
||||
"@readerMatchOf": {"placeholders": {"current": {"type": "int"}, "total": {"type": "int"}}},
|
||||
"readerPreviousMatch": "Previous match",
|
||||
"readerNextMatch": "Next match",
|
||||
"readerCloseSearch": "Close search",
|
||||
"readerExpand": "Expand",
|
||||
"readerCollapse": "Collapse",
|
||||
"readerTabFlashcards": "Flashcards",
|
||||
"readerTabNotes": "Notes",
|
||||
"readerCardCreated": "Flashcard created",
|
||||
"readerCardDeleteTitle": "Delete flashcard?",
|
||||
"readerCardDeleteMessage": "The flashcard is deleted, with how it was studied.",
|
||||
"readerCardDeleteConfirm": "Delete",
|
||||
"readerFrontMatter": "Front matter",
|
||||
"readerPage": "Page {page}",
|
||||
"@readerPage": {"placeholders": {"page": {"type": "int"}}},
|
||||
"readerByPage": "Page",
|
||||
"readerByChapter": "Chapter",
|
||||
"readerCardsEmpty": "Select text to make flashcards in no time",
|
||||
"readerCardsEmptyHint": "Select the lines that matter and send them to a question or an answer, or press \"Create flashcard\".",
|
||||
"readerCreateCard": "Create flashcard",
|
||||
"readerEditCard": "Edit flashcard",
|
||||
"readerBackToCards": "Back to flashcards",
|
||||
"readerReferencedPage": "Referenced page: {page}",
|
||||
"@readerReferencedPage": {"placeholders": {"page": {"type": "int"}}},
|
||||
"readerUseCurrentPage": "Use current ({page})",
|
||||
"@readerUseCurrentPage": {"placeholders": {"page": {"type": "int"}}},
|
||||
"readerQuestion": "Question",
|
||||
"readerAnswer": "Answer",
|
||||
"readerKeepOpen": "Keep creation mode open",
|
||||
"readerClear": "Clear",
|
||||
"readerCreate": "Create",
|
||||
"readerUntitledCard": "Untitled flashcard",
|
||||
"readerCardOptions": "Card options",
|
||||
"readerNoteSaved": "Saved",
|
||||
"readerNoteEditing": "Editing…",
|
||||
"readerNoteSaving": "Saving…",
|
||||
"readerNoteFailed": "Save failed",
|
||||
"readerToQuestion": "Question",
|
||||
"readerToAnswer": "Answer",
|
||||
"readerToNote": "Notes",
|
||||
"readerLineBreaksJoined": "Line breaks joined (tap to keep)",
|
||||
"readerLineBreaksKept": "Line breaks kept (tap to join)",
|
||||
"blockScreenshot": "Screenshot of the page",
|
||||
"readerCaptureCancel": "Cancel screenshot",
|
||||
"readerCaptureHint": "Drag over the part of the page to keep",
|
||||
"readerCaptureFailed": "The page couldn't be drawn. Try again once it has loaded.",
|
||||
"readerScreenshotSaved": "Screenshot saved",
|
||||
"imageOpenPdfPage": "Open the PDF at this page",
|
||||
"homeWidgetDeadlines": "Reading deadlines",
|
||||
"deadlinesTitle": "Reading deadlines",
|
||||
"deadlinesCurrent": "Current",
|
||||
"deadlinesUpcoming": "Upcoming",
|
||||
"deadlinesProgress": "p.{furthest}/{target} · {when}",
|
||||
"@deadlinesProgress": {"placeholders": {"furthest": {"type": "int"}, "target": {"type": "int"}, "when": {"type": "String"}}},
|
||||
"deadlinePassedShort": "deadline passed",
|
||||
"deadlineDueTodayShort": "due today",
|
||||
"deadlineDaysLeft": "{days, plural, =1{1 day left} other{{days} days left}}",
|
||||
"@deadlineDaysLeft": {"placeholders": {"days": {"type": "int"}}},
|
||||
"deadlineToPageToday": "to p.{page} today",
|
||||
"@deadlineToPageToday": {"placeholders": {"page": {"type": "int"}}},
|
||||
"deadlineStartsIn": "Starts in {days, plural, =1{1 day} other{{days} days}}",
|
||||
"@deadlineStartsIn": {"placeholders": {"days": {"type": "int"}}},
|
||||
"deadlinesPlaceholder": "Appears when you have reading deadlines",
|
||||
"deadlinesOpenTimeline": "Open the timeline",
|
||||
"timelineTitle": "Reading timeline",
|
||||
"timelineEmpty": "No reading deadlines yet",
|
||||
"timelineEmptyHint": "Set a reading deadline on a PDF to see it here.",
|
||||
"timelineToday": "Today",
|
||||
"timelinePages": "p.{furthest}/{target}",
|
||||
"@timelinePages": {"placeholders": {"furthest": {"type": "int"}, "target": {"type": "int"}}},
|
||||
"chapterFinished": "Finished “{chapter}” — study the {count, plural, =1{card} other{{count} cards}} you made?",
|
||||
"@chapterFinished": {"placeholders": {"chapter": {"type": "String"}, "count": {"type": "int"}}},
|
||||
"chapterStudyNow": "Study now",
|
||||
"chapterDismiss": "Dismiss",
|
||||
"deadlineChapterLoading": "Loading chapters…",
|
||||
"fileSizeKb": "{size} KB",
|
||||
"@fileSizeKb": {"placeholders": {"size": {"type": "int"}}},
|
||||
"fileSizeMb": "{size} MB",
|
||||
"@fileSizeMb": {"placeholders": {"size": {"type": "String"}}},
|
||||
"pdfPageShort": "p. {page}",
|
||||
"@pdfPageShort": {
|
||||
"placeholders": {
|
||||
"page": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"ankiImportMenu": "Import from Anki",
|
||||
"ankiImportFileType": "Anki decks",
|
||||
"ankiImportTitle": "Import Anki deck",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/path_runn
|
|||
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/step_editor_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/bloc/note_editor_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/widget/note_editor_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/widget/pdf_reader_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/widget/reading_timeline_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/widget/settings_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/bloc/study_session_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/study_focus.dart';
|
||||
|
|
@ -175,6 +177,18 @@ GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
|
|||
state.pathParameters['stepId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutePaths.readingTimeline,
|
||||
builder: (context, state) => const ReadingTimelineScreen(),
|
||||
),
|
||||
// A PDF, read in the whole window; at a page when asked.
|
||||
GoRoute(
|
||||
path: '/pdfs/:pdfId',
|
||||
builder: (context, state) => PdfReaderScreen(
|
||||
pdfId: state.pathParameters['pdfId']!,
|
||||
page: int.tryParse(state.uri.queryParameters['page'] ?? ''),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutePaths.settings,
|
||||
builder: (context, state) => const SettingsScreen(),
|
||||
|
|
|
|||
|
|
@ -27,4 +27,8 @@ abstract interface class AssetGateway() {
|
|||
required int byteSize,
|
||||
required ByteData payload,
|
||||
});
|
||||
|
||||
/// Blanks the row [id]'s payload, then deletes the row: its file goes from
|
||||
/// the server once nothing refers to it any more.
|
||||
Future<void> delete(String id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,19 @@ class AssetRepository(
|
|||
_triggers = uploadTriggers.listen((_) => unawaited(uploadPending()));
|
||||
}
|
||||
|
||||
/// The largest file the server takes.
|
||||
/// The largest file the server takes, of any kind but a PDF.
|
||||
static const maxBytes = 20 * 1024 * 1024;
|
||||
|
||||
/// The largest PDF the server takes (decided 2026-10-03).
|
||||
static const maxPdfBytes = 50 * 1024 * 1024;
|
||||
|
||||
/// A PDF's media type.
|
||||
static const pdfMediaType = 'application/pdf';
|
||||
|
||||
/// The largest file of [mimeType] the server takes.
|
||||
static int maxBytesOf(String mimeType) =>
|
||||
mimeType == pdfMediaType ? maxPdfBytes : maxBytes;
|
||||
|
||||
late final StreamSubscription<void> _triggers;
|
||||
|
||||
/// Adding one file at a time, so a file added twice at once is one row.
|
||||
|
|
@ -46,13 +56,14 @@ class AssetRepository(
|
|||
|
||||
/// Adds [bytes], a file named [fileName] of [mimeType], and returns the id
|
||||
/// of its asset row: the row of the same file if there is one, else a new
|
||||
/// one. Throws an [ArgumentError] for an empty file or one over [maxBytes].
|
||||
/// one. Throws an [ArgumentError] for an empty file or one too large for
|
||||
/// its kind ([maxBytesOf]).
|
||||
Future<String> add(
|
||||
Uint8List bytes, {
|
||||
required String fileName,
|
||||
required String mimeType,
|
||||
}) async {
|
||||
if (bytes.isEmpty || bytes.length > maxBytes) {
|
||||
if (bytes.isEmpty || bytes.length > maxBytesOf(mimeType)) {
|
||||
throw ArgumentError.value(bytes.length, 'bytes', 'is out of range');
|
||||
}
|
||||
return await _adding.run(() async {
|
||||
|
|
@ -183,6 +194,14 @@ class AssetRepository(
|
|||
}
|
||||
}
|
||||
|
||||
/// The size of the file [id] in bytes, from its row; null without one.
|
||||
Future<int?> byteSizeOf(String id) async => (await _rows.byId(id))?.byteSize;
|
||||
|
||||
/// Deletes the row of the file [id], blanked first: the file goes from the
|
||||
/// server once nothing refers to it, and stops taking the person's room.
|
||||
/// Only for files no content refers to any more.
|
||||
Future<void> delete(String id) => _rows.delete(id);
|
||||
|
||||
/// Stops uploading; an upload under way finishes first.
|
||||
Future<void> dispose() async {
|
||||
await _triggers.cancel();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
|
||||
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||||
|
||||
|
|
@ -51,6 +52,24 @@ class SyncAssetGateway(
|
|||
return asset.id!.uuid;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {
|
||||
final uuid = protocol.UuidValue.withValidation(id);
|
||||
if (await protocol.Asset.db.findById(_db, uuid) case final asset?) {
|
||||
// The name it came with is content (ADR 0002, rule 2).
|
||||
await protocol.Asset.db.updateRow(
|
||||
_db,
|
||||
asset.copyWith(payload: redactedPayload),
|
||||
columns: (t) => [t.payload],
|
||||
);
|
||||
await protocol.Asset.db.deleteWhere(
|
||||
_db,
|
||||
where: (t) => t.id.equals(uuid),
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
}
|
||||
|
||||
AssetRow? _row(protocol.Asset? asset) => asset == null
|
||||
? null
|
||||
: (
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@
|
|||
/// A sound: the person's file at `url` (`assetUrl`), played on a tap.
|
||||
const audioBlockType = 'audio';
|
||||
|
||||
/// On the editor's own `image` block: the id of the PDF a screenshot was
|
||||
/// taken from; missing for any other image.
|
||||
const imagePdfKey = 'pdf';
|
||||
|
||||
/// On the editor's own `image` block: the page of that PDF, from 1.
|
||||
const imagePageKey = 'page';
|
||||
|
||||
/// A boxed aside of a kind (a note, a tip, a warning…), holding blocks of
|
||||
/// its own as children.
|
||||
const calloutBlockType = 'callout';
|
||||
|
|
|
|||
20
halkyon_learn_flutter/lib/src/core/content/pdf_page_ref.dart
Normal file
20
halkyon_learn_flutter/lib/src/core/content/pdf_page_ref.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/// A page of a PDF that content is tied to: a card made from it, a note
|
||||
/// about it. The PDF is named by its row's id; pages count from 1.
|
||||
typedef PdfPageRef = ({String pdfId, int page});
|
||||
|
||||
/// [ref] as content payloads store it: `{"pdf", "page"}`.
|
||||
Map<String, Object?> pdfPageRefToJson(PdfPageRef ref) => {
|
||||
'pdf': ref.pdfId,
|
||||
'page': ref.page,
|
||||
};
|
||||
|
||||
/// The page [json] names, as [pdfPageRefToJson] wrote it; null for null.
|
||||
/// Throws a [FormatException] for anything else.
|
||||
PdfPageRef? pdfPageRefFromJson(Object? json) => switch (json) {
|
||||
null => null,
|
||||
{'pdf': final String pdfId, 'page': final int page} when page > 0 => (
|
||||
pdfId: pdfId,
|
||||
page: page,
|
||||
),
|
||||
_ => throw const FormatException('A malformed page of a PDF.'),
|
||||
};
|
||||
|
|
@ -30,6 +30,11 @@ import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_path_a
|
|||
import 'package:halkyon_learn_flutter/src/feature/note/data/note_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/data/sync_note_gateway.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_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdfrx_engine.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/sync_pdf_source_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/sync_settings_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||
|
|
@ -75,6 +80,9 @@ class Dependencies({
|
|||
/// Plays the YouTube videos in content.
|
||||
required final YoutubeVideos videos,
|
||||
|
||||
/// Opens and shows PDFs.
|
||||
required final PdfEngine pdfEngine,
|
||||
|
||||
/// Reads Anki packages to import.
|
||||
required final AnkiPackageReader ankiPackages,
|
||||
final Future<void> Function()? _onDispose,
|
||||
|
|
@ -101,6 +109,7 @@ class Dependencies({
|
|||
// person's database has closed, so their folder can go. If it doesn't
|
||||
// close, the app goes on after a while and the folder goes next time.
|
||||
var previousClosed = Future<void>.value();
|
||||
await PdfrxEngine.initialize();
|
||||
final sounds = AudioplayersSoundPlayer();
|
||||
return Dependencies(
|
||||
auth: AuthRepository(authGateway),
|
||||
|
|
@ -109,6 +118,7 @@ class Dependencies({
|
|||
sounds: sounds,
|
||||
files: FileSelectorFilePicker(),
|
||||
videos: IframeYoutubeVideos(),
|
||||
pdfEngine: PdfrxEngine(),
|
||||
ankiPackages: IsolateAnkiPackageReader(),
|
||||
openPerson: (person) async {
|
||||
await previousClosed.timeout(
|
||||
|
|
@ -181,16 +191,27 @@ Future<PersonDependencies> _openPerson(
|
|||
);
|
||||
final settings = SettingsRepository(SyncSettingsGateway(db, sync));
|
||||
final reviews = ReviewRepository(SyncReviewGateway(db, sync));
|
||||
final deletions = StudysetDeletions(studysets, cards, notes, paths);
|
||||
final noteDeletions = NoteDeletions(notes);
|
||||
final pathDeletions = LearningPathDeletions(paths);
|
||||
final stepDeletions = StepDeletions(paths);
|
||||
final assets = AssetRepository(
|
||||
SyncAssetGateway(db, sync),
|
||||
ServerpodAssetServerGateway(client),
|
||||
DirectoryAssetFileCache(Directory(p.join(folder.path, 'files'))),
|
||||
uploadTriggers: sync.synced,
|
||||
);
|
||||
final pdfSources = PdfSourceRepository(
|
||||
SyncPdfSourceGateway(db, sync),
|
||||
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,
|
||||
|
|
@ -198,6 +219,7 @@ Future<PersonDependencies> _openPerson(
|
|||
notes: notes,
|
||||
paths: paths,
|
||||
pathProgress: pathProgress,
|
||||
pdfSources: pdfSources,
|
||||
reviews: reviews,
|
||||
tags: tags,
|
||||
settings: settings,
|
||||
|
|
@ -206,6 +228,7 @@ Future<PersonDependencies> _openPerson(
|
|||
noteDeletions: noteDeletions,
|
||||
pathDeletions: pathDeletions,
|
||||
stepDeletions: stepDeletions,
|
||||
pdfDeletions: pdfDeletions,
|
||||
onDispose: () async {
|
||||
try {
|
||||
// Deletions first: those still waiting are dropped, those under way
|
||||
|
|
@ -214,6 +237,7 @@ Future<PersonDependencies> _openPerson(
|
|||
await noteDeletions.dispose();
|
||||
await pathDeletions.dispose();
|
||||
await stepDeletions.dispose();
|
||||
await pdfDeletions.dispose();
|
||||
await studysets.dispose();
|
||||
await tags.dispose();
|
||||
await settings.dispose();
|
||||
|
|
@ -222,6 +246,7 @@ Future<PersonDependencies> _openPerson(
|
|||
await notes.dispose();
|
||||
await paths.dispose();
|
||||
await pathProgress.dispose();
|
||||
await pdfSources.dispose();
|
||||
await assets.dispose();
|
||||
await sync.dispose();
|
||||
await db.close();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_pa
|
|||
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_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_source_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.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';
|
||||
|
|
@ -32,6 +34,9 @@ class PersonDependencies({
|
|||
/// Where the person stands in their learning paths.
|
||||
required final PathProgressRepository pathProgress,
|
||||
|
||||
/// The person's PDFs and how far they have read them.
|
||||
required final PdfSourceRepository pdfSources,
|
||||
|
||||
/// The person's reviews.
|
||||
required final ReviewRepository reviews,
|
||||
|
||||
|
|
@ -41,7 +46,7 @@ class PersonDependencies({
|
|||
/// The person's settings, which follow them to every device.
|
||||
required final SettingsRepository settings,
|
||||
|
||||
/// The files of the person's content: images and sounds.
|
||||
/// The files of the person's content: images, sounds and PDFs.
|
||||
required final AssetRepository assets,
|
||||
|
||||
/// The studyset deletions that can still be undone.
|
||||
|
|
@ -55,6 +60,9 @@ class PersonDependencies({
|
|||
|
||||
/// The deletions of paths' steps that can still be undone.
|
||||
required final StepDeletions stepDeletions,
|
||||
|
||||
/// The PDF deletions that can still be undone.
|
||||
required final PdfDeletions pdfDeletions,
|
||||
final Future<void> Function()? _onDispose,
|
||||
}) {
|
||||
/// Closes what was opened for the person.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/bloc/sound_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
||||
|
|
@ -13,6 +14,7 @@ import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.d
|
|||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/atomic_block.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/editor_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
|
|
@ -169,9 +171,22 @@ class _FileBlockState()
|
|||
icon: LucideIcons.imageOff,
|
||||
text: l10n.fileFromElsewhere,
|
||||
),
|
||||
(_FileKind.image, final id?) => _FileImage(
|
||||
assetId: id,
|
||||
maxWidth: (node.attributes[ImageBlockKeys.width] as num?)?.toDouble(),
|
||||
(_FileKind.image, final id?) => Column(
|
||||
children: [
|
||||
_FileImage(
|
||||
assetId: id,
|
||||
maxWidth: (node.attributes[ImageBlockKeys.width] as num?)
|
||||
?.toDouble(),
|
||||
),
|
||||
// A screenshot of a PDF leads back to its page.
|
||||
if ((node.attributes[imagePdfKey], node.attributes[imagePageKey])
|
||||
case (final String pdfId, final int page))
|
||||
TextButton.icon(
|
||||
icon: const Icon(LucideIcons.fileText, size: 16),
|
||||
label: Text(l10n.pdfPageShort(page)),
|
||||
onPressed: () => context.push(RoutePaths.pdf(pdfId, page: page)),
|
||||
),
|
||||
],
|
||||
),
|
||||
(_FileKind.sound, final id?) => _Sound(assetId: id),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ abstract final class RoutePaths() {
|
|||
/// Settings, over the tabs.
|
||||
static const settings = '/settings';
|
||||
|
||||
/// The reading deadlines on a line of days, over the tabs.
|
||||
static const readingTimeline = '/reading-timeline';
|
||||
|
||||
/// A studyset's screen, nested in the library tab: its back button leads
|
||||
/// to the library.
|
||||
static String studyset(String id) => '/library/studysets/$id';
|
||||
|
|
@ -46,6 +49,14 @@ abstract final class RoutePaths() {
|
|||
static String note(String studysetId, String noteId) =>
|
||||
'/studysets/$studysetId/notes/$noteId';
|
||||
|
||||
/// A PDF, to read, over the tabs; at [page] when given, else where reading
|
||||
/// left off. Cards and screenshots tied to a page name only the PDF, which
|
||||
/// knows its studyset.
|
||||
static String pdf(String pdfId, {int? page}) => Uri(
|
||||
path: '/pdfs/$pdfId',
|
||||
queryParameters: page == null ? null : {'page': '$page'},
|
||||
).toString();
|
||||
|
||||
/// A learning path of a studyset, to work through, over the tabs.
|
||||
static String path(String studysetId, String pathId) =>
|
||||
'/studysets/$studysetId/paths/$pathId';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/pdf_page_ref.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
|
||||
|
|
@ -12,9 +13,11 @@ import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
|||
// map), each left out while empty; schema 1 reads as a front/back card.
|
||||
// Schema 3 added where a card came from (`origin`, e.g. "anki"), in its first
|
||||
// revision only and beside its content; left out for a card written here.
|
||||
// Schema 4 added the page of a PDF the card was made from (`source`, see
|
||||
// pdf_page_ref.dart), left out for a card of its own.
|
||||
|
||||
/// The schema this app writes.
|
||||
const cardSchemaVersion = 3;
|
||||
const cardSchemaVersion = 4;
|
||||
|
||||
/// Encodes a card's content into a revision's payload; [origin] names where
|
||||
/// an imported card came from, in its first revision.
|
||||
|
|
@ -45,6 +48,7 @@ ByteData encodeCardPayload(CardContent content, {String? origin}) =>
|
|||
],
|
||||
},
|
||||
'map': ?content.map,
|
||||
if (content.source case final source?) 'source': pdfPageRefToJson(source),
|
||||
});
|
||||
|
||||
/// Where the card of a revision's [payload] came from: null for one written
|
||||
|
|
@ -80,6 +84,7 @@ CardContent decodeCardPayload(ByteData payload) {
|
|||
meanings: _strings(json['meanings'], 'meanings'),
|
||||
matching: _matching(json['matching']),
|
||||
map: _optional<String>(json, 'map'),
|
||||
source: pdfPageRefFromJson(json['source']),
|
||||
);
|
||||
}
|
||||
throw const FormatException('Not a card payload.');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/pdf_page_ref.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/revision_merge.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
|
||||
|
|
@ -46,6 +47,9 @@ enum CardField() {
|
|||
|
||||
/// The map a multiple-choice card is drawn on.
|
||||
map,
|
||||
|
||||
/// The page of a PDF the card was made from.
|
||||
source,
|
||||
}
|
||||
|
||||
/// One answer of a multiple-choice card.
|
||||
|
|
@ -120,6 +124,9 @@ final class const CardContent({
|
|||
|
||||
/// The id of the map a multiple-choice card is drawn on.
|
||||
final String? map,
|
||||
|
||||
/// The page of a PDF the card was made from; kept whatever its type.
|
||||
final PdfPageRef? source,
|
||||
}) with Equatable implements MergeableContent<CardField, CardContent> {
|
||||
/// A front/back card with nothing on either side.
|
||||
factory empty() =>
|
||||
|
|
@ -135,6 +142,7 @@ final class const CardContent({
|
|||
CardField.meanings => meanings,
|
||||
CardField.matching => matching,
|
||||
CardField.map => map,
|
||||
CardField.source => source,
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
@ -147,6 +155,7 @@ final class const CardContent({
|
|||
meanings: field == CardField.meanings ? value! as List<String> : meanings,
|
||||
matching: field == CardField.matching ? value! as Matching : matching,
|
||||
map: field == CardField.map ? value as String? : map,
|
||||
source: field == CardField.source ? value as PdfPageRef? : source,
|
||||
);
|
||||
|
||||
/// This content with some fields replaced; [withField] clears the map.
|
||||
|
|
@ -159,6 +168,7 @@ final class const CardContent({
|
|||
List<String>? meanings,
|
||||
Matching? matching,
|
||||
String? map,
|
||||
PdfPageRef? source,
|
||||
}) => CardContent(
|
||||
type: type ?? this.type,
|
||||
front: front ?? this.front,
|
||||
|
|
@ -168,9 +178,11 @@ final class const CardContent({
|
|||
meanings: meanings ?? this.meanings,
|
||||
matching: matching ?? this.matching,
|
||||
map: map ?? this.map,
|
||||
source: source ?? this.source,
|
||||
);
|
||||
|
||||
/// The fields [type] uses; the others stay empty.
|
||||
/// The fields [type] uses; the others stay empty. The page it was made
|
||||
/// from isn't one of them: every type keeps it.
|
||||
Set<CardField> get fields => switch (type) {
|
||||
CardType.frontBack => const {CardField.front, CardField.back},
|
||||
CardType.multipleChoice when map != null => const {
|
||||
|
|
@ -196,6 +208,7 @@ final class const CardContent({
|
|||
type: type,
|
||||
front: RichContent.empty(),
|
||||
back: RichContent.empty(),
|
||||
source: source,
|
||||
);
|
||||
for (final field in kept) {
|
||||
content = content.withField(field, this[field]);
|
||||
|
|
@ -276,6 +289,7 @@ final class const CardContent({
|
|||
meanings,
|
||||
matching,
|
||||
map,
|
||||
source,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -487,6 +487,7 @@ class const _Conflicts({required final CardDraft draft})
|
|||
CardField.meanings => l10n.vocabularyMeanings,
|
||||
CardField.matching => l10n.composerKindMatching,
|
||||
CardField.map => l10n.composerKindMap,
|
||||
CardField.source => l10n.pdfPageField,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -512,6 +513,9 @@ class const _Version({required final Object? version}) extends StatelessWidget {
|
|||
].join('\n'),
|
||||
),
|
||||
final CardType type => Text(type.name),
|
||||
(pdfId: _, page: final int page) => Text(
|
||||
AppLocalizations.of(context).pdfPageShort(page),
|
||||
),
|
||||
_ => Text('${version ?? ''}'),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ enum HomeWidget() {
|
|||
/// The study goal.
|
||||
goal,
|
||||
|
||||
/// The reading deadlines of PDFs; it shows only while there are some.
|
||||
deadlines,
|
||||
|
||||
/// The week overview.
|
||||
week,
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:halkyon_learn_flutter/src/feature/home/widget/activity_heatmap.d
|
|||
import 'package:halkyon_learn_flutter/src/feature/home/widget/goal_card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/streak.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/week_overview.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/widget/reading_deadlines_card.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
|
|
@ -120,6 +121,7 @@ class _HomeState() extends State<_Home> {
|
|||
progress: state.goalProgress,
|
||||
onChanged: (goal) => _send(HomeEventGoalSet(goal)),
|
||||
),
|
||||
HomeWidget.deadlines => ReadingDeadlinesCard(placeholder: _arranging),
|
||||
HomeWidget.week => SectionCard(
|
||||
padding: const EdgeInsets.fromLTRB(4, 12, 4, 12),
|
||||
child: WeekOverview(
|
||||
|
|
@ -209,6 +211,7 @@ class const _Arranging({
|
|||
final l10n = AppLocalizations.of(context);
|
||||
String label(HomeWidget widget) => switch (widget) {
|
||||
HomeWidget.goal => l10n.homeWidgetGoal,
|
||||
HomeWidget.deadlines => l10n.homeWidgetDeadlines,
|
||||
HomeWidget.week => l10n.homeWidgetWeek,
|
||||
HomeWidget.activity => l10n.homeWidgetActivity,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/pdf_page_ref.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
|
||||
|
||||
// A note revision's content as its synced row stores it (see
|
||||
// core/content/payload). Schema 1: the title and the body.
|
||||
// core/content/payload). Schema 1: the title and the body. Schema 2 added the
|
||||
// page of a PDF the note is about (`source`, see pdf_page_ref.dart), left out
|
||||
// for a note of its own.
|
||||
|
||||
/// The schema this app writes.
|
||||
const noteSchemaVersion = 1;
|
||||
const noteSchemaVersion = 2;
|
||||
|
||||
/// Encodes a note's content into a revision's payload.
|
||||
ByteData encodeNotePayload(NoteContent content) =>
|
||||
encodePayload(noteSchemaVersion, {
|
||||
'title': content.title,
|
||||
'body': content.body.toJson(),
|
||||
if (content.source case final source?) 'source': pdfPageRefToJson(source),
|
||||
});
|
||||
|
||||
/// Decodes a note revision's payload.
|
||||
|
|
@ -27,7 +31,11 @@ NoteContent decodeNotePayload(ByteData payload) {
|
|||
'title': final String title,
|
||||
'body': final Map<String, Object?> body,
|
||||
}) {
|
||||
return NoteContent(title: title, body: RichContent.fromJson(body));
|
||||
return NoteContent(
|
||||
title: title,
|
||||
body: RichContent.fromJson(body),
|
||||
source: pdfPageRefFromJson(json['source']),
|
||||
);
|
||||
}
|
||||
throw const FormatException('Not a note payload.');
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue