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
97 changed files with 34926 additions and 58 deletions

View file

@ -84,7 +84,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
- **One graph per context.** The dependency graph belongs to one instance and one signed-in person; when either changes, a `KeyedSubtree` disposes the whole subtree and rebuilds it. Why: nothing from one person's session (blocs, streams, caches) can leak into another's.
- **App-wide vs. per person.** `Dependencies` holds what outlives a person (the client, auth). Everything of the signed-in person (their local database, its sync, their repositories) is `PersonDependencies`, opened by `PersonScope` inside `SessionGate`'s keyed subtree (`Dependencies.openPerson`) and disposed with it; widgets reach it through `PersonScope.of(context)`.
- **Every person has an app of their own** (`PersonApp`, `lib/src/app/`), which `SessionGate` builds inside the keyed subtree: a `MaterialApp.router` with its own `GoRouter`, and `PersonScope` around the router's navigator (`MaterialApp.builder`). So every screen and dialog is inside the person's scope and reads their repositories from `PersonScope.of(context)` wherever it is built, and a change of person disposes the router with every screen on it and starts again at home. Until someone is signed in, `SessionGate` shows a plain `MaterialApp`: the spinner, or the failure with a way to try again.
- **Platform plugins sit behind small interfaces in `Dependencies`**, with fakes in tests: picking files (`FilePicker`, `file_selector`), playing sounds (`SoundPlayer`, `audioplayers`), playing YouTube videos (`YoutubeVideos`, `youtube_player_iframe` where a web view exists, else the browser), reading Anki packages on another isolate (`AnkiPackageReader`; a widget test's fake clock can't wait for one).
- **Platform plugins sit behind small interfaces in `Dependencies`**, with fakes in tests: picking files (`FilePicker`, `file_selector`), playing sounds (`SoundPlayer`, `audioplayers`), playing YouTube videos (`YoutubeVideos`, `youtube_player_iframe` where a web view exists, else the browser), reading PDFs (`PdfEngine`, pdfrx on PDFium, which `flutter test` can't load: widget tests use `FakePdfEngine`, whose PDF bytes say their pages and chapters), reading Anki packages on another isolate (`AnkiPackageReader`; a widget test's fake clock can't wait for one).
- **Material's `Card` widget clashes with the domain's `Card`**: files that need both import `material_ui` with `hide Card`.
- **State: `flutter_bloc`, uniformly.** Blocs fold repository streams into immutable states with value equality, are provided as close to their consumer as possible, and log through one global `BlocObserver`. Why: the data layer is stream-shaped, event → state machines test without widgets, and one pattern everywhere is itself the point.
- **Value equality comes from `equatable`**, mixed in (`with Equatable`), for models and states alike; `leancode_lint`'s `missing_equatable_props` catches a field left out of `props`.
@ -139,6 +139,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
- **Order is a fractional index** (`fractional_indexing_dart`, rocicorp's algorithm) in its own column, last writer wins. Two devices appending at once can pick the same key, so ties sort by id.
- **Tree, tags, modes, settings and files** (ADR 0004): a studyset's `parent` and sibling `position` form the tree, and studysets in a cycle or without their parent show at the top level (`topLevel`); its `archivedAt`, `starredAt` and `updatedAt` are columns, and every change of a studyset writes `updatedAt` with it (the library lists starred studysets first, then the most recently changed); a card's tags are `card_tag` rows, one per assignment, never updated, and a duplicate counts once; `review_log.mode` is `review`, `reverse` (its own schedule) or `cram` (never moves a schedule), and a schedule replays the reviews of one mode; `person_settings` is meant as one row per person, and extra rows fold into the one with the smallest id; an `asset` row stands for a file in private cloud storage, found by its hash. **Never derive a row's id from a person or anything else guessable**: ids are unique across the whole table, so someone could claim it first in their own space and break the owner's sync.
- **Files** (ADR 0004, rule 5): `AssetEndpoint` (`requireLogin`) takes uploads into the caller's own space by SHA-256 (`startUpload` gives Serverpod's upload description, or null when the file is there already and claims it anew; `finishUpload` keeps it only if its content has that hash) and serves an asset row's file to its space's owner and members only, `NotFoundException` otherwise, also for a deleted row (the sync engine hides tombstones). Files live in the private database storage at `spaces/<space>/<sha256>`, at most 20 MB each, and within limits (`file_limits.dart`, from the environment: an anonymous person 100 MB, an identified one 500 MB or what a `file_allowance` row grants them, the instance 15 GB), checked when an upload starts (unfinished uploads count, or they could fill the disk unnoticed) and again when it finishes (only what is kept counts), refused with `FileLimitExceededException`; `deletePerson` deletes the person's files and allowance, and `FilePruningFutureCall` deletes daily those no live row refers to (or never finished), 7 days after they were last claimed. In the app, `AssetRepository` (`core/asset/`) keeps files by hash in the person's folder (`files/`, uploads waiting marked in `files/uploads/`), uploads after each sync that went through, and downloads on demand, checking the hash; every hash is checked to be one before it becomes part of a path. Content names a file by the URL `asset:<asset row id>` (`assetUrl`, `RichContent.assetIds`): images are the editor's `image` block, sounds an `audio` block (`core/editor/file_blocks.dart`), and any other URL is never fetched. `StoredFileImage` loads through the repository like Flutter's `NetworkImage`, keyed by the person's repository too, so no image crosses people.
- **PDFs** (`feature/pdf_source/`): a `pdf_source` row per PDF in a studyset (payload: title, file name, the asset of its file, page count, reading deadline; last writer wins) and an append-only `pdf_reading` log (`reached` with a page, `reset`): the furthest page is the furthest reached since the last reset, so devices never conflict. A PDF's file is an asset of up to 50 MB (other files 20 MB): the server takes up to 50 MB and deletes a stored file over 20 MB that doesn't start with `%PDF-`. Deleting a PDF deletes its asset row too unless another PDF has the same file (asset rows are otherwise never deleted). Cards and notes tied to a page keep it in their payload (`source`, `core/content/pdf_page_ref.dart`; card schema 4, note schema 2); a screenshot is an `image` block with `pdf` and `page` attributes.
- **Imported cards** say where they came from in their first revision's payload (`origin`, card schema 3, e.g. `anki`); the dashboard doesn't count them as written. The Anki import (`feature/anki/`) reads `.apkg`/`.colpkg` packages of every format on an isolate of its own (`AnkiPackageReader`), zstd through our fork of `pure_zstd` (Anki's frames don't record their size), and its fixtures come from Anki itself (`tool/anki/make_fixtures.py`).
- **Every client migration keeps a device's rows** (`test/core/sync/device_migration_test.dart` replays each on a database with a row in every synced table of ours; SQLite rebuilds tables to add foreign keys, which Serverpod's runner does with foreign keys off). A new synced table needs a row in that test, which fails until it has one.
- **Every synced table indexes `spaceId`** (`indexes: <table>_space_idx: fields: spaceId`, e.g. `studyset.spy.yaml`): deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables.
@ -150,4 +151,6 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
- **One person's data per device**: `PersonFolders` wipes every other person's folder when a person's data opens: their sessions ended on this device. Opening waits for the previous person's database to close.
- **Session logs are not persisted** (`persistentEnabled: false` in every run mode) for privacy. Don't re-enable them.
- **Secrets** live in `halkyon_learn_server/config/passwords.yaml` (git-ignored). Never change `serverSideSessionKeyHashPepper`: it would sign everyone out, and anonymous accounts can't sign back in.
- **Checks on a device** live in `halkyon_learn_flutter/integration_test/` and run by hand, never in `tool/check.sh` or CI: `flutter test integration_test/<file> -d <device>`. `pdf_engine_test.dart` checks the real PDF engine (on the Linux desktop too), `youtube_player_test.dart` the embedded YouTube player (Android). Never on the emulator from an agent session: it takes the editor down with it.
- **Windows builds need Developer Mode** (pdfrx's build makes symbolic links). PDFium itself comes with the build as a Dart native asset.
- **Linux builds need** `libwebkit2gtk-4.1-dev`, `libsecret-1-dev`, and for sounds (`audioplayers`) `libgstreamer1.0-dev` and `libgstreamer-plugins-base1.0-dev`. If a Linux build tries to install to `/usr/local`, the CMake cache is stale: delete `build/linux`.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,260 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "pdf_reading",
"dartName": "PdfReading",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "pdfSourceId",
"fieldName": "pdfSourceId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "readAt",
"fieldName": "readAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "kind",
"fieldName": "kind",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "page",
"fieldName": "page",
"columnType": 6,
"isNullable": false,
"dartType": "int"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "pdf_reading_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "pdf_reading_fk_1",
"columns": [
"pdfSourceId"
],
"referenceTable": "pdf_source",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "pdf_reading_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "pdf_reading_source_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "pdfSourceId"
},
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "readAt"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "pdf_source",
"dartName": "PdfSource",
"module": "halkyon_learn",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "spaceId",
"fieldName": "spaceId",
"columnType": 6,
"isNullable": true,
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "studysetId",
"fieldName": "studysetId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "keyId",
"fieldName": "keyId",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "payload",
"fieldName": "payload",
"columnType": 5,
"isNullable": false,
"dartType": "dart:typed_data:ByteData"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "nonce",
"fieldName": "nonce",
"columnType": 5,
"isNullable": true,
"dartType": "dart:typed_data:ByteData?"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "pdf_source_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "pdf_source_fk_1",
"columns": [
"studysetId"
],
"referenceTable": "studyset",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4,
"deferrable": "initiallyDeferred"
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "pdf_source_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "pdf_source_studyset_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "studysetId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -10,6 +10,7 @@ part '20260930145136285-reviews/migration.dart';
part '20261001064926911-tree-tags-assets/migration.dart';
part '20261002191122122-notes/migration.dart';
part '20261002211346701-learning-paths/migration.dart';
part '20261003110855698-pdf-sources/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -22,6 +23,7 @@ class MigrationRegistry {
_Migration20261001064926911_tree_tags_assets(),
_Migration20261002191122122_notes(),
_Migration20261002211346701_learning_paths(),
_Migration20261003110855698_pdf_sources(),
];
/// List of all client-side database migration versions.

View file

@ -0,0 +1,782 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: dead_code, unnecessary_null_comparison
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import '../pdf_source/pdf_source.dart' as _ijdmmu1w;
/// How far a PDF has been read, as an entry that is never changed once written:
/// the furthest page isn't stored but derived from these, as a card's schedule
/// is from its reviews, so reading on two devices never conflicts. An entry is
/// written only when the furthest page grows, so a PDF has at most an entry per
/// page per fresh start. Synced; the entry goes with its PDF.
abstract class PdfReading
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
PdfReading._({
this.id,
this.spaceId,
required this.pdfSourceId,
this.pdfSource,
required this.readAt,
required this.kind,
required this.page,
});
factory PdfReading({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue pdfSourceId,
_ijdmmu1w.PdfSource? pdfSource,
required DateTime readAt,
required String kind,
required int page,
}) = _PdfReadingImpl;
factory PdfReading.fromJson(Map<String, dynamic> jsonSerialization) {
return PdfReading(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
pdfSourceId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['pdfSourceId'],
),
pdfSource: jsonSerialization['pdfSource'] == null
? null
: _itx5j3f5.Protocol().deserialize<_ijdmmu1w.PdfSource>(
jsonSerialization['pdfSource'],
),
readAt: _isc.DateTimeJsonExtension.fromJson(jsonSerialization['readAt']),
kind: jsonSerialization['kind'] as String,
page: jsonSerialization['page'] as int,
);
}
static final t = PdfReadingTable();
static const db = PdfReadingRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue pdfSourceId;
/// The PDF read.
_ijdmmu1w.PdfSource? pdfSource;
/// When, in UTC.
DateTime readAt;
/// 'reached' for a page reached (marking the PDF finished reaches its last
/// page); 'reset' for a fresh start: the furthest page is the furthest
/// reached since the last one.
String kind;
/// The page reached, from 1; 0 for 'reset'.
int page;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [PdfReading]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
PdfReading copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? pdfSourceId,
_ijdmmu1w.PdfSource? pdfSource,
DateTime? readAt,
String? kind,
int? page,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'PdfReading',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'pdfSourceId': pdfSourceId.toJson(),
if (pdfSource != null) 'pdfSource': pdfSource?.toJson(),
'readAt': readAt.toJson(),
'kind': kind,
'page': page,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'PdfReading',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'pdfSourceId': pdfSourceId.toJson(),
if (pdfSource != null) 'pdfSource': pdfSource?.toJsonForProtocol(),
'readAt': readAt.toJson(),
'kind': kind,
'page': page,
};
}
static PdfReadingInclude include({_ijdmmu1w.PdfSourceInclude? pdfSource}) {
return PdfReadingInclude._(pdfSource: pdfSource);
}
static PdfReadingIncludeList includeList({
_isd.WhereExpressionBuilder<PdfReadingTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<PdfReadingTable>? orderBy,
_isd.OrderByListBuilder<PdfReadingTable>? orderByList,
PdfReadingInclude? include,
}) {
return PdfReadingIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(PdfReading.t),
orderByList: orderByList?.call(PdfReading.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _PdfReadingImpl extends PdfReading {
_PdfReadingImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue pdfSourceId,
_ijdmmu1w.PdfSource? pdfSource,
required DateTime readAt,
required String kind,
required int page,
}) : super._(
id: id,
spaceId: spaceId,
pdfSourceId: pdfSourceId,
pdfSource: pdfSource,
readAt: readAt,
kind: kind,
page: page,
);
/// Returns a shallow copy of this [PdfReading]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
PdfReading copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? pdfSourceId,
Object? pdfSource = _Undefined,
DateTime? readAt,
String? kind,
int? page,
}) {
return PdfReading(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
pdfSourceId: pdfSourceId ?? this.pdfSourceId,
pdfSource: pdfSource is _ijdmmu1w.PdfSource?
? pdfSource
: this.pdfSource?.copyWith(),
readAt: readAt ?? this.readAt,
kind: kind ?? this.kind,
page: page ?? this.page,
);
}
}
class PdfReadingUpdateTable extends _isd.UpdateTable<PdfReadingTable> {
PdfReadingUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> pdfSourceId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.pdfSourceId,
value,
);
_isd.ColumnValue<DateTime, DateTime> readAt(DateTime value) =>
_isd.ColumnValue(
table.readAt,
value,
);
_isd.ColumnValue<String, String> kind(String value) => _isd.ColumnValue(
table.kind,
value,
);
_isd.ColumnValue<int, int> page(int value) => _isd.ColumnValue(
table.page,
value,
);
}
class PdfReadingTable extends _isd.Table<_isc.UuidValue?> {
PdfReadingTable({super.tableRelation}) : super(tableName: 'pdf_reading') {
updateTable = PdfReadingUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
pdfSourceId = _isd.ColumnUuid(
'pdfSourceId',
this,
);
readAt = _isd.ColumnDateTime(
'readAt',
this,
);
kind = _isd.ColumnString(
'kind',
this,
);
page = _isd.ColumnInt(
'page',
this,
);
}
late final PdfReadingUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid pdfSourceId;
/// The PDF read.
_ijdmmu1w.PdfSourceTable? _pdfSource;
/// When, in UTC.
late final _isd.ColumnDateTime readAt;
/// 'reached' for a page reached (marking the PDF finished reaches its last
/// page); 'reset' for a fresh start: the furthest page is the furthest
/// reached since the last one.
late final _isd.ColumnString kind;
/// The page reached, from 1; 0 for 'reset'.
late final _isd.ColumnInt page;
_ijdmmu1w.PdfSourceTable get pdfSource {
if (_pdfSource != null) return _pdfSource!;
_pdfSource = _isd.createRelationTable(
relationFieldName: 'pdfSource',
field: PdfReading.t.pdfSourceId,
foreignField: _ijdmmu1w.PdfSource.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_ijdmmu1w.PdfSourceTable(tableRelation: foreignTableRelation),
);
return _pdfSource!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
pdfSourceId,
readAt,
kind,
page,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'pdfSource') {
return pdfSource;
}
return null;
}
}
class PdfReadingInclude extends _isd.IncludeObject {
PdfReadingInclude._({_ijdmmu1w.PdfSourceInclude? pdfSource}) {
_pdfSource = pdfSource;
}
_ijdmmu1w.PdfSourceInclude? _pdfSource;
@override
Map<String, _isd.Include?> get includes => {'pdfSource': _pdfSource};
@override
_isd.Table<_isc.UuidValue?> get table => PdfReading.t;
}
class PdfReadingIncludeList extends _isd.IncludeList {
PdfReadingIncludeList._({
_isd.WhereExpressionBuilder<PdfReadingTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(PdfReading.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => PdfReading.t;
}
class PdfReadingRepository {
const PdfReadingRepository._();
final attachRow = const PdfReadingAttachRowRepository._();
/// Returns a list of [PdfReading]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<PdfReading>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PdfReadingTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<PdfReadingTable>? orderBy,
_isd.OrderByListBuilder<PdfReadingTable>? orderByList,
_isd.Transaction? transaction,
PdfReadingInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<PdfReading>(
where: where?.call(PdfReading.t),
orderBy: orderBy?.call(PdfReading.t),
orderByList: orderByList?.call(PdfReading.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [PdfReading] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<PdfReading?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PdfReadingTable>? where,
int? offset,
_isd.OrderByBuilder<PdfReadingTable>? orderBy,
_isd.OrderByListBuilder<PdfReadingTable>? orderByList,
_isd.Transaction? transaction,
PdfReadingInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<PdfReading>(
where: where?.call(PdfReading.t),
orderBy: orderBy?.call(PdfReading.t),
orderByList: orderByList?.call(PdfReading.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [PdfReading] by its [id] or null if no such row exists.
Future<PdfReading?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
PdfReadingInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<PdfReading>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [PdfReading]s in the list and returns the inserted rows.
///
/// The returned [PdfReading]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfReading>> insert(
_isd.DatabaseSession session,
List<PdfReading> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<PdfReading>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [PdfReading] and returns the inserted row.
///
/// The returned [PdfReading] will have its `id` field set.
Future<PdfReading> insertRow(
_isd.DatabaseSession session,
PdfReading row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<PdfReading>(
row,
transaction: transaction,
);
}
/// Upserts all [PdfReading]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [PdfReading]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfReading>> upsert(
_isd.DatabaseSession session,
List<PdfReading> rows, {
required _isd.ColumnSelections<PdfReadingTable> conflictColumns,
_isd.ColumnSelections<PdfReadingTable>? updateColumns,
_isd.WhereExpressionBuilder<PdfReadingTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<PdfReading>(
rows,
conflictColumns: conflictColumns(PdfReading.t),
updateColumns: updateColumns?.call(PdfReading.t),
updateWhere: updateWhere?.call(PdfReading.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [PdfReading] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [PdfReading] will have its `id` field set.
Future<PdfReading?> upsertRow(
_isd.DatabaseSession session,
PdfReading row, {
required _isd.ColumnSelections<PdfReadingTable> conflictColumns,
_isd.ColumnSelections<PdfReadingTable>? updateColumns,
_isd.WhereExpressionBuilder<PdfReadingTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<PdfReading>(
row,
conflictColumns: conflictColumns(PdfReading.t),
updateColumns: updateColumns?.call(PdfReading.t),
updateWhere: updateWhere?.call(PdfReading.t),
transaction: transaction,
);
}
/// Updates all [PdfReading]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfReading>> update(
_isd.DatabaseSession session,
List<PdfReading> rows, {
_isd.ColumnSelections<PdfReadingTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<PdfReading>(
rows,
columns: columns?.call(PdfReading.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [PdfReading]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<PdfReading> updateRow(
_isd.DatabaseSession session,
PdfReading row, {
_isd.ColumnSelections<PdfReadingTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<PdfReading>(
row,
columns: columns?.call(PdfReading.t),
transaction: transaction,
);
}
/// Updates a single [PdfReading] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<PdfReading?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<PdfReadingUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<PdfReading>(
id,
columnValues: columnValues(PdfReading.t.updateTable),
transaction: transaction,
);
}
/// Updates all [PdfReading]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfReading>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<PdfReadingUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<PdfReadingTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<PdfReadingTable>? orderBy,
_isd.OrderByListBuilder<PdfReadingTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<PdfReading>(
columnValues: columnValues(PdfReading.t.updateTable),
where: where(PdfReading.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(PdfReading.t),
orderByList: orderByList?.call(PdfReading.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [PdfReading]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfReading>> delete(
_isd.DatabaseSession session,
List<PdfReading> rows, {
_isd.OrderByBuilder<PdfReadingTable>? orderBy,
_isd.OrderByListBuilder<PdfReadingTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<PdfReading>(
rows,
orderBy: orderBy?.call(PdfReading.t),
orderByList: orderByList?.call(PdfReading.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [PdfReading].
Future<PdfReading> deleteRow(
_isd.DatabaseSession session,
PdfReading row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<PdfReading>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfReading>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<PdfReadingTable> where,
_isd.OrderByBuilder<PdfReadingTable>? orderBy,
_isd.OrderByListBuilder<PdfReadingTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<PdfReading>(
where: where(PdfReading.t),
orderBy: orderBy?.call(PdfReading.t),
orderByList: orderByList?.call(PdfReading.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PdfReadingTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<PdfReading>(
where: where?.call(PdfReading.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [PdfReading] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<PdfReadingTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<PdfReading>(
where: where(PdfReading.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class PdfReadingAttachRowRepository {
const PdfReadingAttachRowRepository._();
/// Creates a relation between the given [PdfReading] and [PdfSource]
/// by setting the [PdfReading]'s foreign key `pdfSourceId` to refer to the [PdfSource].
Future<void> pdfSource(
_isd.DatabaseSession session,
PdfReading pdfReading,
_ijdmmu1w.PdfSource pdfSource, {
_isd.Transaction? transaction,
}) async {
if (pdfReading.id == null) {
throw ArgumentError.notNull('pdfReading.id');
}
if (pdfSource.id == null) {
throw ArgumentError.notNull('pdfSource.id');
}
var $pdfReading = pdfReading.copyWith(pdfSourceId: pdfSource.id);
await session.db.updateRow<PdfReading>(
$pdfReading,
columns: [PdfReading.t.pdfSourceId],
transaction: transaction,
);
}
}

View file

@ -0,0 +1,790 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: dead_code, unnecessary_null_comparison
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import '../studyset/studyset.dart' as _ie5k1jc2;
/// A PDF in a studyset, to read and learn from: its cards and notes can be tied
/// to its pages. Synced; its space owns it.
///
/// Its content (title, the name the file came with, the file, its page count
/// once known, a reading deadline) lives in the payload, last writer wins, as a
/// learning path's: rarely edited on two devices at once. The file itself is
/// an asset (ADR 0004), named in the payload by the asset row's id.
abstract class PdfSource
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
PdfSource._({
this.id,
this.spaceId,
required this.studysetId,
this.studyset,
this.keyId,
required this.payload,
this.nonce,
});
factory PdfSource({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue studysetId,
_ie5k1jc2.Studyset? studyset,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _PdfSourceImpl;
factory PdfSource.fromJson(Map<String, dynamic> jsonSerialization) {
return PdfSource(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
studysetId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['studysetId'],
),
studyset: jsonSerialization['studyset'] == null
? null
: _itx5j3f5.Protocol().deserialize<_ie5k1jc2.Studyset>(
jsonSerialization['studyset'],
),
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = PdfSourceTable();
static const db = PdfSourceRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
_isc.UuidValue studysetId;
/// The studyset the PDF belongs to; the PDF goes with it.
_ie5k1jc2.Studyset? studyset;
/// The encryption key's id; always null (ADR 0002, rule 6).
String? keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "title", "fileName", "asset",
/// "pageCount", "deadline"}; the app's pdf_source_payload.dart describes each
/// schema.
_idt.ByteData payload;
/// The encryption nonce; always null, like keyId.
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [PdfSource]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
PdfSource copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? studysetId,
_ie5k1jc2.Studyset? studyset,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'PdfSource',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'studysetId': studysetId.toJson(),
if (studyset != null) 'studyset': studyset?.toJson(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'PdfSource',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'studysetId': studysetId.toJson(),
if (studyset != null) 'studyset': studyset?.toJsonForProtocol(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static PdfSourceInclude include({_ie5k1jc2.StudysetInclude? studyset}) {
return PdfSourceInclude._(studyset: studyset);
}
static PdfSourceIncludeList includeList({
_isd.WhereExpressionBuilder<PdfSourceTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<PdfSourceTable>? orderBy,
_isd.OrderByListBuilder<PdfSourceTable>? orderByList,
PdfSourceInclude? include,
}) {
return PdfSourceIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(PdfSource.t),
orderByList: orderByList?.call(PdfSource.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _PdfSourceImpl extends PdfSource {
_PdfSourceImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue studysetId,
_ie5k1jc2.Studyset? studyset,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
studysetId: studysetId,
studyset: studyset,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [PdfSource]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
PdfSource copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? studysetId,
Object? studyset = _Undefined,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return PdfSource(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
studysetId: studysetId ?? this.studysetId,
studyset: studyset is _ie5k1jc2.Studyset?
? studyset
: this.studyset?.copyWith(),
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class PdfSourceUpdateTable extends _isd.UpdateTable<PdfSourceTable> {
PdfSourceUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> studysetId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.studysetId,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class PdfSourceTable extends _isd.Table<_isc.UuidValue?> {
PdfSourceTable({super.tableRelation}) : super(tableName: 'pdf_source') {
updateTable = PdfSourceUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
studysetId = _isd.ColumnUuid(
'studysetId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final PdfSourceUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
late final _isd.ColumnUuid studysetId;
/// The studyset the PDF belongs to; the PDF goes with it.
_ie5k1jc2.StudysetTable? _studyset;
/// The encryption key's id; always null (ADR 0002, rule 6).
late final _isd.ColumnString keyId;
/// The content as UTF-8 JSON: {"schemaVersion", "title", "fileName", "asset",
/// "pageCount", "deadline"}; the app's pdf_source_payload.dart describes each
/// schema.
late final _isd.ColumnByteData payload;
/// The encryption nonce; always null, like keyId.
late final _isd.ColumnByteData nonce;
_ie5k1jc2.StudysetTable get studyset {
if (_studyset != null) return _studyset!;
_studyset = _isd.createRelationTable(
relationFieldName: 'studyset',
field: PdfSource.t.studysetId,
foreignField: _ie5k1jc2.Studyset.t.id,
tableRelation: tableRelation,
createTable: (foreignTableRelation) =>
_ie5k1jc2.StudysetTable(tableRelation: foreignTableRelation),
);
return _studyset!;
}
@override
List<_isd.Column> get columns => [
id,
spaceId,
studysetId,
keyId,
payload,
nonce,
];
@override
_isd.Table? getRelationTable(String relationField) {
if (relationField == 'studyset') {
return studyset;
}
return null;
}
}
class PdfSourceInclude extends _isd.IncludeObject {
PdfSourceInclude._({_ie5k1jc2.StudysetInclude? studyset}) {
_studyset = studyset;
}
_ie5k1jc2.StudysetInclude? _studyset;
@override
Map<String, _isd.Include?> get includes => {'studyset': _studyset};
@override
_isd.Table<_isc.UuidValue?> get table => PdfSource.t;
}
class PdfSourceIncludeList extends _isd.IncludeList {
PdfSourceIncludeList._({
_isd.WhereExpressionBuilder<PdfSourceTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(PdfSource.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => PdfSource.t;
}
class PdfSourceRepository {
const PdfSourceRepository._();
final attachRow = const PdfSourceAttachRowRepository._();
/// Returns a list of [PdfSource]s matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order of the items use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// The maximum number of items can be set by [limit]. If no limit is set,
/// all items matching the query will be returned.
///
/// [offset] defines how many items to skip, after which [limit] (or all)
/// items are read from the database.
///
/// ```dart
/// var persons = await Persons.db.find(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.firstName,
/// limit: 100,
/// );
/// ```
Future<List<PdfSource>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PdfSourceTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<PdfSourceTable>? orderBy,
_isd.OrderByListBuilder<PdfSourceTable>? orderByList,
_isd.Transaction? transaction,
PdfSourceInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<PdfSource>(
where: where?.call(PdfSource.t),
orderBy: orderBy?.call(PdfSource.t),
orderByList: orderByList?.call(PdfSource.t),
limit: limit,
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [PdfSource] matching the given query parameters.
///
/// Use [where] to specify which items to include in the return value.
/// If none is specified, all items will be returned.
///
/// To specify the order use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// [offset] defines how many items to skip, after which the next one will be picked.
///
/// ```dart
/// var youngestPerson = await Persons.db.findFirstRow(
/// session,
/// where: (t) => t.lastName.equals('Jones'),
/// orderBy: (t) => t.age,
/// );
/// ```
Future<PdfSource?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PdfSourceTable>? where,
int? offset,
_isd.OrderByBuilder<PdfSourceTable>? orderBy,
_isd.OrderByListBuilder<PdfSourceTable>? orderByList,
_isd.Transaction? transaction,
PdfSourceInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<PdfSource>(
where: where?.call(PdfSource.t),
orderBy: orderBy?.call(PdfSource.t),
orderByList: orderByList?.call(PdfSource.t),
offset: offset,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [PdfSource] by its [id] or null if no such row exists.
Future<PdfSource?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
PdfSourceInclude? include,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<PdfSource>(
id,
transaction: transaction,
include: include,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [PdfSource]s in the list and returns the inserted rows.
///
/// The returned [PdfSource]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails to
/// insert, none of the rows will be inserted.
///
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
/// rows are silently skipped, and only the successfully inserted rows are
/// returned.
///
/// If [noReturn] is set to `true`, the inserted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfSource>> insert(
_isd.DatabaseSession session,
List<PdfSource> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<PdfSource>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [PdfSource] and returns the inserted row.
///
/// The returned [PdfSource] will have its `id` field set.
Future<PdfSource> insertRow(
_isd.DatabaseSession session,
PdfSource row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<PdfSource>(
row,
transaction: transaction,
);
}
/// Upserts all [PdfSource]s in the list and returns the resulting rows.
///
/// If a row conflicts on the given [conflictColumns], the existing row is
/// updated with the new values. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies to rows matching the
/// given expression. Conflicting rows that don't match are skipped and not
/// returned, so the resulting list may be shorter than [rows].
///
/// The returned [PdfSource]s will have their `id` fields set.
///
/// This is an atomic operation, meaning that if one of the rows fails,
/// none of the rows will be affected.
///
/// If [noReturn] is set to `true`, the resulting rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfSource>> upsert(
_isd.DatabaseSession session,
List<PdfSource> rows, {
required _isd.ColumnSelections<PdfSourceTable> conflictColumns,
_isd.ColumnSelections<PdfSourceTable>? updateColumns,
_isd.WhereExpressionBuilder<PdfSourceTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<PdfSource>(
rows,
conflictColumns: conflictColumns(PdfSource.t),
updateColumns: updateColumns?.call(PdfSource.t),
updateWhere: updateWhere?.call(PdfSource.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [PdfSource] and returns the resulting row.
///
/// If the row conflicts on the given [conflictColumns], the existing row is
/// updated. Otherwise, a new row is inserted.
///
/// If [updateColumns] is provided, only those columns will be updated on
/// conflict. If null, all non-conflict, non-id columns are updated.
///
/// If [updateWhere] is provided, the update only applies when the existing
/// row matches the expression. Returns `null` if no row was affected — for
/// example when [updateWhere] does not match the conflicting row.
///
/// The returned [PdfSource] will have its `id` field set.
Future<PdfSource?> upsertRow(
_isd.DatabaseSession session,
PdfSource row, {
required _isd.ColumnSelections<PdfSourceTable> conflictColumns,
_isd.ColumnSelections<PdfSourceTable>? updateColumns,
_isd.WhereExpressionBuilder<PdfSourceTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<PdfSource>(
row,
conflictColumns: conflictColumns(PdfSource.t),
updateColumns: updateColumns?.call(PdfSource.t),
updateWhere: updateWhere?.call(PdfSource.t),
transaction: transaction,
);
}
/// Updates all [PdfSource]s in the list and returns the updated rows. If
/// [columns] is provided, only those columns will be updated. Defaults to
/// all columns.
/// This is an atomic operation, meaning that if one of the rows fails to
/// update, none of the rows will be updated.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfSource>> update(
_isd.DatabaseSession session,
List<PdfSource> rows, {
_isd.ColumnSelections<PdfSourceTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<PdfSource>(
rows,
columns: columns?.call(PdfSource.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [PdfSource]. The row needs to have its id set.
/// Optionally, a list of [columns] can be provided to only update those
/// columns. Defaults to all columns.
Future<PdfSource> updateRow(
_isd.DatabaseSession session,
PdfSource row, {
_isd.ColumnSelections<PdfSourceTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<PdfSource>(
row,
columns: columns?.call(PdfSource.t),
transaction: transaction,
);
}
/// Updates a single [PdfSource] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<PdfSource?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<PdfSourceUpdateTable> columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<PdfSource>(
id,
columnValues: columnValues(PdfSource.t.updateTable),
transaction: transaction,
);
}
/// Updates all [PdfSource]s matching the [where] expression with the specified [columnValues].
/// Returns the list of updated rows.
///
/// If [noReturn] is set to `true`, the updated rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfSource>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<PdfSourceUpdateTable> columnValues,
required _isd.WhereExpressionBuilder<PdfSourceTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<PdfSourceTable>? orderBy,
_isd.OrderByListBuilder<PdfSourceTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<PdfSource>(
columnValues: columnValues(PdfSource.t.updateTable),
where: where(PdfSource.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(PdfSource.t),
orderByList: orderByList?.call(PdfSource.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [PdfSource]s in the list and returns the deleted rows.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// This is an atomic operation, meaning that if one of the rows fail to
/// be deleted, none of the rows will be deleted.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfSource>> delete(
_isd.DatabaseSession session,
List<PdfSource> rows, {
_isd.OrderByBuilder<PdfSourceTable>? orderBy,
_isd.OrderByListBuilder<PdfSourceTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<PdfSource>(
rows,
orderBy: orderBy?.call(PdfSource.t),
orderByList: orderByList?.call(PdfSource.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [PdfSource].
Future<PdfSource> deleteRow(
_isd.DatabaseSession session,
PdfSource row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<PdfSource>(
row,
transaction: transaction,
);
}
/// Deletes all rows matching the [where] expression.
///
/// To specify the order of the returned rows use [orderBy] or [orderByList]
/// when sorting by multiple columns.
///
/// If [noReturn] is set to `true`, the deleted rows are not read back from
/// the database and an empty list is returned. This avoids the overhead of
/// transferring and deserializing the rows when the result is not needed.
Future<List<PdfSource>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<PdfSourceTable> where,
_isd.OrderByBuilder<PdfSourceTable>? orderBy,
_isd.OrderByListBuilder<PdfSourceTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<PdfSource>(
where: where(PdfSource.t),
orderBy: orderBy?.call(PdfSource.t),
orderByList: orderByList?.call(PdfSource.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Counts the number of rows matching the [where] expression. If omitted,
/// will return the count of all rows in the table.
Future<int> count(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<PdfSourceTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<PdfSource>(
where: where?.call(PdfSource.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [PdfSource] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<PdfSourceTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<PdfSource>(
where: where(PdfSource.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}
class PdfSourceAttachRowRepository {
const PdfSourceAttachRowRepository._();
/// Creates a relation between the given [PdfSource] and [Studyset]
/// by setting the [PdfSource]'s foreign key `studysetId` to refer to the [Studyset].
Future<void> studyset(
_isd.DatabaseSession session,
PdfSource pdfSource,
_ie5k1jc2.Studyset studyset, {
_isd.Transaction? transaction,
}) async {
if (pdfSource.id == null) {
throw ArgumentError.notNull('pdfSource.id');
}
if (studyset.id == null) {
throw ArgumentError.notNull('studyset.id');
}
var $pdfSource = pdfSource.copyWith(studysetId: studyset.id);
await session.db.updateRow<PdfSource>(
$pdfSource,
columns: [PdfSource.t.studysetId],
transaction: transaction,
);
}
}

View file

@ -38,6 +38,8 @@ import 'learning_path/learning_path_step.dart' as _irnpk9dc;
import 'learning_path/learning_path_step_revision.dart' as _i59qfu1l;
import 'note/note.dart' as _i9njjhyv;
import 'note/note_revision.dart' as _i6j5dng4;
import 'pdf_source/pdf_reading.dart' as _i4l20c7i;
import 'pdf_source/pdf_source.dart' as _iz838ro9;
import 'review/review_log.dart' as _irzjbdqz;
import 'settings/person_settings.dart' as _irnhx5e5;
import 'studyset/studyset.dart' as _ikbclmae;
@ -62,6 +64,8 @@ export 'learning_path/learning_path_step.dart';
export 'learning_path/learning_path_step_revision.dart';
export 'note/note.dart';
export 'note/note_revision.dart';
export 'pdf_source/pdf_reading.dart';
export 'pdf_source/pdf_source.dart';
export 'review/review_log.dart';
export 'settings/person_settings.dart';
export 'studyset/studyset.dart';
@ -1335,6 +1339,204 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'pdf_reading',
dartName: 'PdfReading',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'pdfSourceId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'readAt',
columnType: _isd.ColumnType.timestampWithoutTimeZone,
isNullable: false,
dartType: 'DateTime',
),
_isd.ColumnDefinition(
name: 'kind',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isd.ColumnDefinition(
name: 'page',
columnType: _isd.ColumnType.bigint,
isNullable: false,
dartType: 'int',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'pdf_reading_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
_isd.ForeignKeyDefinition(
constraintName: 'pdf_reading_fk_1',
columns: ['pdfSourceId'],
referenceTable: 'pdf_source',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'pdf_reading_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'pdf_reading_source_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'pdfSourceId',
),
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'readAt',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'pdf_source',
dartName: 'PdfSource',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'studysetId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'pdf_source_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
_isd.ForeignKeyDefinition(
constraintName: 'pdf_source_fk_1',
columns: ['studysetId'],
referenceTable: 'studyset',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
deferrable: _isd.DeferrableConstraint.initiallyDeferred,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'pdf_source_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'pdf_source_studyset_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'studysetId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'person_settings',
dartName: 'PersonSettings',
@ -1795,6 +1997,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _i6j5dng4.NoteRevision) {
return _i6j5dng4.NoteRevision.fromJson(data) as T;
}
if (t == _i4l20c7i.PdfReading) {
return _i4l20c7i.PdfReading.fromJson(data) as T;
}
if (t == _iz838ro9.PdfSource) {
return _iz838ro9.PdfSource.fromJson(data) as T;
}
if (t == _irzjbdqz.ReviewLog) {
return _irzjbdqz.ReviewLog.fromJson(data) as T;
}
@ -1883,6 +2091,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_i6j5dng4.NoteRevision?>()) {
return (data != null ? _i6j5dng4.NoteRevision.fromJson(data) : null) as T;
}
if (t == _isc.getType<_i4l20c7i.PdfReading?>()) {
return (data != null ? _i4l20c7i.PdfReading.fromJson(data) : null) as T;
}
if (t == _isc.getType<_iz838ro9.PdfSource?>()) {
return (data != null ? _iz838ro9.PdfSource.fromJson(data) : null) as T;
}
if (t == _isc.getType<_irzjbdqz.ReviewLog?>()) {
return (data != null ? _irzjbdqz.ReviewLog.fromJson(data) : null) as T;
}
@ -1947,6 +2161,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
_i59qfu1l.LearningPathStepRevision => 'LearningPathStepRevision',
_i9njjhyv.Note => 'Note',
_i6j5dng4.NoteRevision => 'NoteRevision',
_i4l20c7i.PdfReading => 'PdfReading',
_iz838ro9.PdfSource => 'PdfSource',
_irzjbdqz.ReviewLog => 'ReviewLog',
_irnhx5e5.PersonSettings => 'PersonSettings',
_ikbclmae.Studyset => 'Studyset',
@ -2005,6 +2221,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'Note';
case _i6j5dng4.NoteRevision():
return 'NoteRevision';
case _i4l20c7i.PdfReading():
return 'PdfReading';
case _iz838ro9.PdfSource():
return 'PdfSource';
case _irzjbdqz.ReviewLog():
return 'ReviewLog';
case _irnhx5e5.PersonSettings():
@ -2098,6 +2318,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'NoteRevision') {
return deserialize<_i6j5dng4.NoteRevision>(data['data']);
}
if (dataClassName == 'PdfReading') {
return deserialize<_i4l20c7i.PdfReading>(data['data']);
}
if (dataClassName == 'PdfSource') {
return deserialize<_iz838ro9.PdfSource>(data['data']);
}
if (dataClassName == 'ReviewLog') {
return deserialize<_irzjbdqz.ReviewLog>(data['data']);
}
@ -2188,6 +2414,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
return _i9njjhyv.Note.t;
case _i6j5dng4.NoteRevision:
return _i6j5dng4.NoteRevision.t;
case _i4l20c7i.PdfReading:
return _i4l20c7i.PdfReading.t;
case _iz838ro9.PdfSource:
return _iz838ro9.PdfSource.t;
case _irzjbdqz.ReviewLog:
return _irzjbdqz.ReviewLog.t;
case _irnhx5e5.PersonSettings:

View file

@ -20,6 +20,8 @@ import 'learning_path/learning_path_step.dart' as _irnpk9dc;
import 'learning_path/learning_path_step_revision.dart' as _i59qfu1l;
import 'note/note.dart' as _i9njjhyv;
import 'note/note_revision.dart' as _i6j5dng4;
import 'pdf_source/pdf_reading.dart' as _i4l20c7i;
import 'pdf_source/pdf_source.dart' as _iz838ro9;
import 'review/review_log.dart' as _irzjbdqz;
import 'settings/person_settings.dart' as _irnhx5e5;
import 'studyset/studyset.dart' as _ikbclmae;
@ -44,6 +46,8 @@ final List<_isd.Table> syncTables = [
_i9njjhyv.Note.t,
_i6j5dng4.NoteRevision.t,
_itkfn57x.NoteTag.t,
_i4l20c7i.PdfReading.t,
_iz838ro9.PdfSource.t,
_irnhx5e5.PersonSettings.t,
_irzjbdqz.ReviewLog.t,
_ikbclmae.Studyset.t,

View 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()));
}

View file

@ -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",

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:

View file

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

View file

@ -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(),

View file

@ -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);
}

View file

@ -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();

View file

@ -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
: (

View file

@ -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';

View 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.'),
};

Some files were not shown because too many files have changed in this diff Show more