mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(koreader): rebuild the plugin interface and fix four-way annotation sync
Squash of feat/koreader-widget-board. KOReader plugin 2.0.0 gets the widget-board dashboard, redesigned book details, menus, lists and dialogs, Decor page and reading companions. Highlights, notes and bookmarks now stay in step across web, Kobo, KOReader and the phones (defects D-00 to D-20, migration 0121), and reading progress and narration hand off cleanly between devices.
This commit is contained in:
@@ -238,6 +238,9 @@ export interface AnnotationHubBookFacet {
|
||||
export const ANNOTATION_BULK_ACTIONS = ["trash", "restore", "restyle", "star", "unstar"] as const;
|
||||
export type AnnotationBulkAction = (typeof ANNOTATION_BULK_ACTIONS)[number];
|
||||
|
||||
/** `errorCode` on the 409 from `DELETE /annotations/:id` while a synced device has not acknowledged the deletion. */
|
||||
export const ANNOTATION_PURGE_PENDING_DEVICE_SYNC = "ANNOTATION_PURGE_PENDING_DEVICE_SYNC";
|
||||
|
||||
export const ANNOTATION_COLOR_NAME_MAX_ENTRIES = 32;
|
||||
export const ANNOTATION_COLOR_NAME_MAX_LENGTH = 40;
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ export const SERVER_FEATURES = [
|
||||
"read-aloud-delivery",
|
||||
"koreader-delivery-settings",
|
||||
"server-connection-identity",
|
||||
/** `GET /books/:id/annotations` and `/bookmarks` accept `readerFileId` and return positions for that file. */
|
||||
"reader-file-positions",
|
||||
] as const;
|
||||
|
||||
export type ServerFeature = (typeof SERVER_FEATURES)[number];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReadAloudDeliveryPreference, ReadAloudDeliveryVariant, DeliveryArtifactMetadata } from "./delivery";
|
||||
import type { HighlightOfTheDayWidgetData, ReadingGoalWidgetData, ReadingStreakWidgetData } from "./dashboard";
|
||||
import type { SeriesIndex } from "./series-index";
|
||||
import type { AchievementRarity } from "./achievement";
|
||||
|
||||
export interface KoreaderCredentials {
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
@@ -184,6 +185,45 @@ export type KoreaderCatalogSettableReadStatus = "unread" | "want_to_read" | "rea
|
||||
export interface KoreaderCatalogSeriesSummary {
|
||||
total: number;
|
||||
finished: number;
|
||||
/** Books of the series in the reading bucket. Absent from servers that predate it. */
|
||||
reading?: number;
|
||||
}
|
||||
|
||||
// Requested only once the server advertises the catalogListsV2 capability: an
|
||||
// older server rejects the parameters as unknown.
|
||||
export type KoreaderCatalogSectionSort = "name" | "name_desc" | "books";
|
||||
|
||||
export type KoreaderCatalogSectionFilter = "reading" | "finished";
|
||||
|
||||
export type KoreaderCatalogMemberState = "finished" | "reading" | "unread";
|
||||
|
||||
export interface KoreaderCatalogPreviewBook {
|
||||
id: number;
|
||||
title: string;
|
||||
hasCover: boolean;
|
||||
thumbnailUrl: string | null;
|
||||
seriesIndex: SeriesIndex | null;
|
||||
progressPercentage: number | null;
|
||||
readStatus: string | null;
|
||||
lastReadAt: string | null;
|
||||
addedAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** `key` is "A" to "Z" or "#"; `index` is the 0-based offset of the first entry with that key in the list's current order. */
|
||||
export interface KoreaderCatalogJumpBucket {
|
||||
key: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogAuthorSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
birthYear: number | null;
|
||||
deathYear: number | null;
|
||||
hasPhoto: boolean;
|
||||
photoUrl: string | null;
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogEntry {
|
||||
@@ -196,6 +236,19 @@ export interface KoreaderCatalogEntry {
|
||||
seriesId?: number;
|
||||
href?: string;
|
||||
booksHref?: string;
|
||||
// List-page extras. Optional so a plugin reading an older server renders plain rows.
|
||||
authorId?: number;
|
||||
hasPhoto?: boolean;
|
||||
photoUrl?: string | null;
|
||||
previewBooks?: KoreaderCatalogPreviewBook[];
|
||||
readingCount?: number;
|
||||
finishedCount?: number;
|
||||
seriesName?: string | null;
|
||||
authorNames?: string[];
|
||||
memberStates?: KoreaderCatalogMemberState[];
|
||||
expectedCount?: number | null;
|
||||
/** Jump-bucket key of this entry's sort key, identical to the key its bucket carries. */
|
||||
letter?: string;
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogFile {
|
||||
@@ -211,6 +264,12 @@ export interface KoreaderCatalogFile {
|
||||
role: string;
|
||||
downloadVariant: "original" | "audioless_epub";
|
||||
sizeBytes: number | null;
|
||||
/**
|
||||
* The stored file's size, for display. sizeBytes is the size of what the
|
||||
* download will be and stays null for a variant built on request; this lets
|
||||
* a reader see roughly how big a plain EPUB is. Absent from older servers.
|
||||
*/
|
||||
sourceSizeBytes?: number | null;
|
||||
durationSeconds: number | null;
|
||||
downloadUrl: string;
|
||||
devicePath: string;
|
||||
@@ -240,6 +299,8 @@ export interface KoreaderCatalogBookListItem {
|
||||
detailUrl: string;
|
||||
addedAt: string;
|
||||
updatedAt: string;
|
||||
/** Only on the dashboard's finishedThisYear entries: the reader's local day the book was finished. */
|
||||
finishedOn?: string | null;
|
||||
}
|
||||
|
||||
export type KoreaderCatalogRelatedSectionId = "series" | "author" | "similar";
|
||||
@@ -257,6 +318,14 @@ export interface KoreaderCatalogRelatedBook {
|
||||
isComic?: boolean;
|
||||
}
|
||||
|
||||
/** One book of the series, for the detail page's series path. */
|
||||
export interface KoreaderCatalogSeriesMember {
|
||||
id: number;
|
||||
title: string | null;
|
||||
seriesIndex: SeriesIndex | null;
|
||||
readStatus: string | null;
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogRelatedSection {
|
||||
id: KoreaderCatalogRelatedSectionId;
|
||||
title: string;
|
||||
@@ -282,6 +351,12 @@ export interface KoreaderCatalogBookDetail extends KoreaderCatalogBookListItem {
|
||||
progress: KoreaderCatalogProgress | null;
|
||||
files: KoreaderCatalogFile[];
|
||||
relatedSections: KoreaderCatalogRelatedSection[];
|
||||
/**
|
||||
* Every book of the series the reader can see, this one included, with its
|
||||
* read status. The series related section stays capped for its covers; this
|
||||
* list is not. Absent from servers that predate it.
|
||||
*/
|
||||
seriesMembers?: KoreaderCatalogSeriesMember[];
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogPage<T> {
|
||||
@@ -289,6 +364,8 @@ export interface KoreaderCatalogPage<T> {
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
// The first item's position, echoed when the request paged by offset.
|
||||
offset?: number;
|
||||
hasNext: boolean;
|
||||
hasPrevious: boolean;
|
||||
nextUrl: string | null;
|
||||
@@ -296,6 +373,10 @@ export interface KoreaderCatalogPage<T> {
|
||||
// Present when the page is scoped to a single series, summarising
|
||||
// read-through progress across the whole series.
|
||||
seriesSummary?: KoreaderCatalogSeriesSummary | null;
|
||||
// Sent only for `buckets=1` on a title or author sort.
|
||||
buckets?: KoreaderCatalogJumpBucket[];
|
||||
// Present when the page is filtered by author; null when no visible author has that name.
|
||||
authorSummary?: KoreaderCatalogAuthorSummary | null;
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogSectionResponse {
|
||||
@@ -303,11 +384,19 @@ export interface KoreaderCatalogSectionResponse {
|
||||
items: KoreaderCatalogEntry[];
|
||||
// Pagination is only emitted for large sections (authors, series).
|
||||
page?: number;
|
||||
// The first entry's position, echoed when the request paged by offset.
|
||||
offset?: number;
|
||||
hasNext?: boolean;
|
||||
hasPrevious?: boolean;
|
||||
nextUrl?: string | null;
|
||||
previousUrl?: string | null;
|
||||
query?: string | null;
|
||||
total?: number;
|
||||
size?: number;
|
||||
// Sent for the name sorts only; the count sort has no letter order to jump through.
|
||||
buckets?: KoreaderCatalogJumpBucket[];
|
||||
sort?: KoreaderCatalogSectionSort;
|
||||
filter?: KoreaderCatalogSectionFilter | null;
|
||||
}
|
||||
|
||||
// The dashboard row below Continue reading is user-selectable. "random"
|
||||
@@ -349,6 +438,47 @@ export interface KoreaderCatalogBrowseCounts {
|
||||
smartScopes: number;
|
||||
}
|
||||
|
||||
// Minutes read per local day in the reader's time zone, oldest first and
|
||||
// ending today. Feeds the KOReader dashboard's year heatmap and week chart.
|
||||
export interface KoreaderReadingYear {
|
||||
startDate: string;
|
||||
minutes: number[];
|
||||
}
|
||||
|
||||
// The book read most in the four weeks around this day a year ago.
|
||||
export interface KoreaderYearAgo {
|
||||
book: KoreaderCatalogBookListItem;
|
||||
minutes: number;
|
||||
days: number;
|
||||
endProgress: number | null;
|
||||
firstDay: string;
|
||||
lastDay: string;
|
||||
}
|
||||
|
||||
// One badge as the device board's Next badge widget draws it. groupKey names a
|
||||
// tiered family (hours_read, pages_read, books_finished, ...) so a client can
|
||||
// tell what the progress counts; it is null for one-off badges.
|
||||
export interface KoreaderAchievementBadge {
|
||||
key: string;
|
||||
groupKey: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
rarity: AchievementRarity;
|
||||
iconName: string;
|
||||
threshold: number | null;
|
||||
currentProgress: number | null;
|
||||
awardedAt: string | null;
|
||||
}
|
||||
|
||||
// The reader's badges at a glance: how many are earned, the unearned badge
|
||||
// with the most progress, and the badge earned most recently.
|
||||
export interface KoreaderAchievementSummary {
|
||||
earned: number;
|
||||
total: number;
|
||||
next: KoreaderAchievementBadge | null;
|
||||
latest: KoreaderAchievementBadge | null;
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogDashboardResponse {
|
||||
generatedAt: string;
|
||||
username: string;
|
||||
@@ -365,6 +495,13 @@ export interface KoreaderCatalogDashboardResponse {
|
||||
readingGoal: ReadingGoalWidgetData;
|
||||
readingStreak: ReadingStreakWidgetData;
|
||||
highlightOfTheDay: HighlightOfTheDayWidgetData | null;
|
||||
// Optional so plugins and servers predating them keep working.
|
||||
readingYear?: KoreaderReadingYear;
|
||||
yearAgo?: KoreaderYearAgo | null;
|
||||
// Books finished this local year, most recently finished first.
|
||||
finishedThisYear?: KoreaderCatalogBookListItem[];
|
||||
// Null when the reader has turned achievements off.
|
||||
achievements?: KoreaderAchievementSummary | null;
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogDiscoverResponse {
|
||||
@@ -420,7 +557,14 @@ export interface KoreaderCatalogManifestPage {
|
||||
}
|
||||
|
||||
export type KoreaderPluginCapability =
|
||||
"catalogBulkManifest" | "catalogDashboardSections" | "bookmarkSync" | "readAloudDeliveryVariants" | "readAloudDeliverySettings";
|
||||
| "catalogBulkManifest"
|
||||
| "catalogDashboardSections"
|
||||
| "bookmarkSync"
|
||||
| "readAloudDeliveryVariants"
|
||||
| "readAloudDeliverySettings"
|
||||
| "catalogListsV2"
|
||||
| "sidecarState"
|
||||
| "annotationPendingBooks";
|
||||
|
||||
export interface KoreaderPluginVersionInfo {
|
||||
pluginVersion: string;
|
||||
|
||||
Reference in New Issue
Block a user