@readium/navigator 2.10.3 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio/AudioNavigator.js +1 -1
- package/dist/audio/AudioPoolManager.js +1 -1
- package/dist/audio/engine/WebAudioEngine.js +1 -1
- package/dist/divina/DivinaNavigator.js +1 -0
- package/dist/divina/DivinaPageManager.js +1 -0
- package/dist/divina/DivinaPagedPresenter.js +1 -0
- package/dist/divina/DivinaPeripherals.js +1 -0
- package/dist/divina/DivinaScrolledPresenter.js +1 -0
- package/dist/divina/DivinaSpreader.js +1 -0
- package/dist/divina/DivinaVariantSelector.js +1 -0
- package/dist/divina/preferences/DivinaDefaults.js +1 -0
- package/dist/divina/preferences/DivinaPreferences.js +1 -0
- package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
- package/dist/divina/preferences/DivinaSettings.js +1 -0
- package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
- package/dist/epub/EpubNavigator.js +1 -1
- package/dist/epub/frame/FrameBlobBuilder.js +3 -1
- package/dist/epub/frame/FrameComms.js +1 -1
- package/dist/epub/frame/FrameManager.js +1 -1
- package/dist/epub/frame/FramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLFrameManager.js +1 -1
- package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLPeripherals.js +2 -2
- package/dist/helpers/serialize.js +1 -0
- package/dist/index.js +1 -1
- package/dist/peripherals/KeyboardConditionBridge.js +1 -1
- package/dist/peripherals/KeyboardPeripherals.js +1 -1
- package/dist/webpub/WebPubFrameManager.js +1 -1
- package/dist/webpub/WebPubFramePoolManager.js +1 -1
- package/dist/webpub/WebPubNavigator.js +1 -1
- package/package.json +5 -5
- package/src/audio/AudioNavigator.ts +107 -5
- package/src/audio/AudioPoolManager.ts +14 -8
- package/src/audio/engine/AudioEngine.ts +34 -1
- package/src/audio/engine/WebAudioEngine.ts +48 -3
- package/src/divina/DivinaNavigator.ts +704 -0
- package/src/divina/DivinaPageManager.ts +322 -0
- package/src/divina/DivinaPagedPresenter.ts +508 -0
- package/src/divina/DivinaPeripherals.ts +630 -0
- package/src/divina/DivinaScrolledPresenter.ts +516 -0
- package/src/divina/DivinaSpreader.ts +202 -0
- package/src/divina/DivinaVariantSelector.ts +77 -0
- package/src/divina/index.ts +8 -0
- package/src/divina/preferences/DivinaDefaults.ts +37 -0
- package/src/divina/preferences/DivinaPreferences.ts +77 -0
- package/src/divina/preferences/DivinaPreferencesEditor.ts +96 -0
- package/src/divina/preferences/DivinaSettings.ts +38 -0
- package/src/divina/preferences/index.ts +4 -0
- package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
- package/src/epub/EpubNavigator.ts +73 -7
- package/src/epub/frame/FrameBlobBuilder.ts +144 -39
- package/src/epub/frame/FrameComms.ts +42 -7
- package/src/epub/frame/FrameManager.ts +27 -16
- package/src/epub/frame/FramePoolManager.ts +106 -48
- package/src/epub/fxl/FXLFrameManager.ts +99 -34
- package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
- package/src/epub/fxl/FXLPeripherals.ts +22 -1
- package/src/helpers/serialize.ts +9 -0
- package/src/index.ts +1 -0
- package/src/peripherals/KeyboardConditionBridge.ts +15 -5
- package/src/peripherals/KeyboardPeripherals.ts +13 -7
- package/src/webpub/WebPubFrameManager.ts +27 -16
- package/src/webpub/WebPubFramePoolManager.ts +5 -2
- package/src/webpub/WebPubNavigator.ts +44 -0
- package/types/src/audio/AudioNavigator.d.ts +29 -0
- package/types/src/audio/engine/AudioEngine.d.ts +27 -1
- package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
- package/types/src/divina/DivinaNavigator.d.ts +119 -0
- package/types/src/divina/DivinaPageManager.d.ts +66 -0
- package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
- package/types/src/divina/DivinaPeripherals.d.ts +121 -0
- package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
- package/types/src/divina/DivinaSpreader.d.ts +65 -0
- package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
- package/types/src/divina/index.d.ts +8 -0
- package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
- package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
- package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
- package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
- package/types/src/divina/preferences/index.d.ts +4 -0
- package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
- package/types/src/epub/EpubNavigator.d.ts +7 -1
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
- package/types/src/epub/frame/FrameComms.d.ts +8 -1
- package/types/src/epub/frame/FrameManager.d.ts +3 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +7 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
- package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
- package/types/src/helpers/serialize.d.ts +1 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +3 -0
- package/types/src/webpub/WebPubNavigator.d.ts +2 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Link, Page, Publication, ReadingProgression } from "@readium/shared";
|
|
2
|
+
import { Orientation, Spread } from "../epub/fxl/FXLSpreader.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single page of a Divina publication, with its resolved layout properties.
|
|
6
|
+
* Unlike FXLSpreader, this model never mutates the publication's links.
|
|
7
|
+
*/
|
|
8
|
+
export class DivinaPage {
|
|
9
|
+
readonly link: Link;
|
|
10
|
+
readonly index: number; // Index in the reading order
|
|
11
|
+
page: Page = Page.center; // Resolved position within a spread
|
|
12
|
+
isLandscape: boolean;
|
|
13
|
+
addBlank = false; // Orphaned single page that must occupy a full spread on its own
|
|
14
|
+
|
|
15
|
+
constructor(link: Link, index: number) {
|
|
16
|
+
this.link = link;
|
|
17
|
+
this.index = index;
|
|
18
|
+
const orientation = link.properties?.otherProperties["orientation"];
|
|
19
|
+
this.isLandscape = orientation === Orientation.landscape ||
|
|
20
|
+
(orientation !== Orientation.portrait && (link.width || 0) > (link.height || 0));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 1-based page number */
|
|
24
|
+
get number(): number {
|
|
25
|
+
return this.index + 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get authoredPage(): Page | undefined {
|
|
29
|
+
return this.link.properties?.page;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whether this page occupies both slots of a spread in double-page mode */
|
|
33
|
+
get double(): boolean {
|
|
34
|
+
return this.page === Page.center || this.isLandscape || this.addBlank;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Computes synthetic spreads for a Divina publication from the `page`
|
|
40
|
+
* (left/right/center) link properties, falling back to computed alternation.
|
|
41
|
+
*
|
|
42
|
+
* Slot model (used by the paged presenter): in double-page mode every spread
|
|
43
|
+
* occupies exactly two slots — a pair is 1+1, and a lone page (center,
|
|
44
|
+
* landscape or orphaned/addBlank) takes a double-width slot. This keeps
|
|
45
|
+
* slide indices (always even) aligned with spread boundaries.
|
|
46
|
+
*/
|
|
47
|
+
export class DivinaSpreader {
|
|
48
|
+
readonly rtl: boolean;
|
|
49
|
+
shift = true; // Whether the first page stands alone (cover)
|
|
50
|
+
pages: DivinaPage[] = [];
|
|
51
|
+
spreads: DivinaPage[][] = [];
|
|
52
|
+
private itemToSlotArr: number[] = [];
|
|
53
|
+
private slotToItemArr: number[] = [];
|
|
54
|
+
private spreadOfItemArr: number[] = [];
|
|
55
|
+
|
|
56
|
+
constructor(publication: Publication) {
|
|
57
|
+
this.rtl = publication.metadata.effectiveReadingProgression === ReadingProgression.rtl;
|
|
58
|
+
this.pages = publication.readingOrder.items.map((link, index) => new DivinaPage(link, index));
|
|
59
|
+
this.index();
|
|
60
|
+
this.testShift();
|
|
61
|
+
this.buildSlots();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private index(redo = false) {
|
|
65
|
+
let nDouble = 0;
|
|
66
|
+
this.pages.forEach((page, index) => {
|
|
67
|
+
if(!page.authoredPage || redo) {
|
|
68
|
+
page.page = page.isLandscape ?
|
|
69
|
+
Page.center :
|
|
70
|
+
((((this.shift ? 0 : 1) + index - nDouble) % 2) ?
|
|
71
|
+
(this.rtl ? Page.right : Page.left) :
|
|
72
|
+
(this.rtl ? Page.left : Page.right));
|
|
73
|
+
} else {
|
|
74
|
+
page.page = page.authoredPage;
|
|
75
|
+
}
|
|
76
|
+
if(page.double)
|
|
77
|
+
nDouble++;
|
|
78
|
+
});
|
|
79
|
+
this.buildSpreads();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private testShift() {
|
|
83
|
+
let wasLastSingle = false;
|
|
84
|
+
this.spreads.forEach((spread, index) => {
|
|
85
|
+
if(spread.length > 1) {
|
|
86
|
+
wasLastSingle = false;
|
|
87
|
+
return; // Only interested in single-page "spreads"
|
|
88
|
+
}
|
|
89
|
+
const single = spread[0];
|
|
90
|
+
|
|
91
|
+
// First page is landscape/spread-both means no shift
|
|
92
|
+
if(index === 0 && (single.isLandscape || single.link.properties?.otherProperties["spread"] === Spread.both))
|
|
93
|
+
this.shift = false;
|
|
94
|
+
|
|
95
|
+
// If the last spread was a true single, and this spread is a center page,
|
|
96
|
+
// the single was an orphaned half of a double page spread: pad it.
|
|
97
|
+
if(wasLastSingle && single.page === Page.center)
|
|
98
|
+
this.spreads[index - 1][0].addBlank = true;
|
|
99
|
+
|
|
100
|
+
// An orphaned component of a double page spread (that's not the first page)
|
|
101
|
+
if(!single.isLandscape && single.page !== Page.center && single.index > 0)
|
|
102
|
+
wasLastSingle = true;
|
|
103
|
+
else
|
|
104
|
+
wasLastSingle = false;
|
|
105
|
+
});
|
|
106
|
+
if(!this.shift) {
|
|
107
|
+
this.spreads = [];
|
|
108
|
+
this.index(true); // Re-index spreads
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private buildSpreads() {
|
|
113
|
+
this.spreads = [];
|
|
114
|
+
let currentSet: DivinaPage[] = [];
|
|
115
|
+
this.pages.forEach((page, index) => {
|
|
116
|
+
if(!index && this.shift) {
|
|
117
|
+
this.spreads.push([page]);
|
|
118
|
+
} else if(page.page === Page.center) {
|
|
119
|
+
// A center (single) page spread: push immediately and reset current set
|
|
120
|
+
if(currentSet.length > 0) this.spreads.push(currentSet);
|
|
121
|
+
this.spreads.push([page]);
|
|
122
|
+
currentSet = [];
|
|
123
|
+
} else if(currentSet.length >= 2) { // Spread has max 2 pages
|
|
124
|
+
this.spreads.push(currentSet);
|
|
125
|
+
currentSet = [page];
|
|
126
|
+
} else
|
|
127
|
+
currentSet.push(page);
|
|
128
|
+
});
|
|
129
|
+
if(currentSet.length > 0) this.spreads.push(currentSet);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Builds the item <-> slot mappings for double-page mode.
|
|
134
|
+
* Slots are consumed in spread order; every spread takes exactly two slots.
|
|
135
|
+
*/
|
|
136
|
+
private buildSlots() {
|
|
137
|
+
this.itemToSlotArr = new Array(this.pages.length);
|
|
138
|
+
this.slotToItemArr = [];
|
|
139
|
+
this.spreadOfItemArr = new Array(this.pages.length);
|
|
140
|
+
let slot = 0;
|
|
141
|
+
this.spreads.forEach((spread, spreadIndex) => {
|
|
142
|
+
spread.forEach((page) => {
|
|
143
|
+
this.itemToSlotArr[page.index] = slot;
|
|
144
|
+
this.spreadOfItemArr[page.index] = spreadIndex;
|
|
145
|
+
const width = spread.length === 1 ? 2 : 1;
|
|
146
|
+
for (let i = 0; i < width; i++)
|
|
147
|
+
this.slotToItemArr[slot + i] = page.index;
|
|
148
|
+
slot += width;
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Total number of slots (spread mode). Always even. */
|
|
154
|
+
get slotCount(): number {
|
|
155
|
+
return this.slotToItemArr.length;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Count of pages that take a double-width slot in spread mode */
|
|
159
|
+
get nDouble(): number {
|
|
160
|
+
return this.pages.reduce((n, p) => n + (p.double ? 1 : 0), 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
slotOfItem(itemIndex: number): number {
|
|
164
|
+
return this.itemToSlotArr[Math.max(0, Math.min(itemIndex, this.pages.length - 1))] ?? 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
itemOfSlot(slot: number): number {
|
|
168
|
+
return this.slotToItemArr[Math.max(0, Math.min(slot, this.slotToItemArr.length - 1))] ?? 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
spreadIndexOfItem(itemIndex: number): number {
|
|
172
|
+
return this.spreadOfItemArr[Math.max(0, Math.min(itemIndex, this.pages.length - 1))] ?? 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
spreadOfItem(itemIndex: number): DivinaPage[] {
|
|
176
|
+
return this.spreads[this.spreadIndexOfItem(itemIndex)];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Whether the page occupies a full spread on its own in double-page mode
|
|
181
|
+
* (and therefore takes a double-width slot). This mirrors buildSlots()
|
|
182
|
+
* exactly: any page alone in its spread is double.
|
|
183
|
+
*/
|
|
184
|
+
isDouble(itemIndex: number): boolean {
|
|
185
|
+
return this.spreadOfItem(itemIndex)?.length === 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
findByHref(href: string): DivinaPage | undefined {
|
|
189
|
+
return this.pages.find(p => p.link.href === href);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The position of a page within a spread, viewport-wise:
|
|
194
|
+
* left/right of the gutter, or centered when displayed alone.
|
|
195
|
+
*/
|
|
196
|
+
spreadPosition(spread: DivinaPage[], target: DivinaPage, perPage: number): Page {
|
|
197
|
+
if(perPage < 2 || spread.length < 2) return Page.center;
|
|
198
|
+
return target === spread[0] ?
|
|
199
|
+
(this.rtl ? Page.right : Page.left) :
|
|
200
|
+
(this.rtl ? Page.left : Page.right);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Link } from "@readium/shared";
|
|
2
|
+
import { sML } from "@readium/helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User-selectable image quality for publications that provide alternate
|
|
6
|
+
* resolutions of their pages (RWPM `alternate` links).
|
|
7
|
+
*/
|
|
8
|
+
export enum DivinaQuality {
|
|
9
|
+
auto = "auto", // Match the display resolution
|
|
10
|
+
low = "low", // Smallest available variant
|
|
11
|
+
high = "high", // One step above the display resolution
|
|
12
|
+
max = "max", // Largest available variant
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Display caps for mobile devices: beyond this, decode lag, memory pressure
|
|
16
|
+
// and canvas limits outweigh the quality gain (from xbreader)
|
|
17
|
+
export const MOBILE_MAX_HEIGHT = 2560;
|
|
18
|
+
export const MOBILE_MAX_WIDTH = 1800;
|
|
19
|
+
|
|
20
|
+
// Accounts for rounding and slight aspect-ratio variance between variants
|
|
21
|
+
const DIMENSION_TOLERANCE = 16;
|
|
22
|
+
|
|
23
|
+
/** The link itself and its (bitmap) alternates, if any */
|
|
24
|
+
export function gatherVariants(item: Link): Link[] {
|
|
25
|
+
const variants = item.alternates?.items ? [...item.alternates.items] : [];
|
|
26
|
+
variants.unshift(item);
|
|
27
|
+
return variants.filter(l => !l.type || l.type.startsWith("image/"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VariantSelectionOptions {
|
|
31
|
+
/** Display size of the page in device pixels (CSS box × devicePixelRatio) */
|
|
32
|
+
targetWidth: number;
|
|
33
|
+
targetHeight: number;
|
|
34
|
+
/** The axis that constrains the display: height in paged mode, width in scrolled mode */
|
|
35
|
+
axis: "width" | "height";
|
|
36
|
+
quality: DivinaQuality;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Selects the most appropriate variant of a page image for the current
|
|
41
|
+
* display, based purely on the choices the manifest offers: the smallest
|
|
42
|
+
* variant that covers the display resolution (in device pixels), biased by
|
|
43
|
+
* the quality preference, and capped on mobile devices.
|
|
44
|
+
*/
|
|
45
|
+
export function selectVariant(item: Link, opts: VariantSelectionOptions): Link {
|
|
46
|
+
const dim = (l: Link) => (opts.axis === "width" ? l.width : l.height) || 0;
|
|
47
|
+
// Variants without dimensions can't be compared, leave them out
|
|
48
|
+
const candidates = gatherVariants(item).filter(l => dim(l) > 0);
|
|
49
|
+
if (candidates.length <= 1) return candidates[0] || item;
|
|
50
|
+
|
|
51
|
+
const sorted = candidates.sort((a, b) => dim(a) - dim(b));
|
|
52
|
+
const mobile = sML.OS.iOS || sML.OS.Android;
|
|
53
|
+
const cap = mobile ? (opts.axis === "width" ? MOBILE_MAX_WIDTH : MOBILE_MAX_HEIGHT) : Infinity;
|
|
54
|
+
/** The largest variant that doesn't exceed the mobile cap (or the smallest overall) */
|
|
55
|
+
const capped = (pick: Link): Link => {
|
|
56
|
+
if (dim(pick) <= cap) return pick;
|
|
57
|
+
const fitting = sorted.filter(l => l === pick || dim(l) <= cap);
|
|
58
|
+
return fitting.length > 1 ? fitting[fitting.length - 2] : sorted[0];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
switch (opts.quality) {
|
|
62
|
+
case DivinaQuality.low:
|
|
63
|
+
return sorted[0];
|
|
64
|
+
case DivinaQuality.max:
|
|
65
|
+
return capped(sorted[sorted.length - 1]);
|
|
66
|
+
default: {
|
|
67
|
+
const target = Math.min(opts.axis === "width" ? opts.targetWidth : opts.targetHeight, cap);
|
|
68
|
+
// Smallest variant that covers the display resolution
|
|
69
|
+
let pick = sorted.find(l => dim(l) >= target - DIMENSION_TOLERANCE) || sorted[sorted.length - 1];
|
|
70
|
+
if (opts.quality === DivinaQuality.high) {
|
|
71
|
+
const next = sorted[sorted.indexOf(pick) + 1];
|
|
72
|
+
if (next) pick = next;
|
|
73
|
+
}
|
|
74
|
+
return capped(pick);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./DivinaNavigator.ts";
|
|
2
|
+
export * from "./DivinaSpreader.ts";
|
|
3
|
+
export * from "./DivinaPageManager.ts";
|
|
4
|
+
export * from "./DivinaPagedPresenter.ts";
|
|
5
|
+
export * from "./DivinaScrolledPresenter.ts";
|
|
6
|
+
export * from "./DivinaPeripherals.ts";
|
|
7
|
+
export * from "./DivinaVariantSelector.ts";
|
|
8
|
+
export * from "./preferences/index.ts";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureBoolean,
|
|
3
|
+
ensureEnumValue,
|
|
4
|
+
ensureNonNegative,
|
|
5
|
+
ensureString,
|
|
6
|
+
ensureValueInRange
|
|
7
|
+
} from "../../preferences/guards.ts";
|
|
8
|
+
|
|
9
|
+
import { DivinaQuality } from "../DivinaVariantSelector.ts";
|
|
10
|
+
import { stripWidthRangeConfig } from "./DivinaPreferences.ts";
|
|
11
|
+
|
|
12
|
+
export interface IDivinaDefaults {
|
|
13
|
+
backgroundColor?: string | null,
|
|
14
|
+
constraint?: number | null,
|
|
15
|
+
quality?: DivinaQuality | null,
|
|
16
|
+
scrolled?: boolean | null,
|
|
17
|
+
spreads?: boolean | null,
|
|
18
|
+
stripWidth?: number | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class DivinaDefaults {
|
|
22
|
+
backgroundColor: string | null;
|
|
23
|
+
constraint: number;
|
|
24
|
+
quality: DivinaQuality;
|
|
25
|
+
scrolled: boolean | null;
|
|
26
|
+
spreads: boolean;
|
|
27
|
+
stripWidth: number;
|
|
28
|
+
|
|
29
|
+
constructor(defaults: IDivinaDefaults) {
|
|
30
|
+
this.backgroundColor = ensureString(defaults.backgroundColor) || null;
|
|
31
|
+
this.constraint = ensureNonNegative(defaults.constraint) || 0;
|
|
32
|
+
this.quality = ensureEnumValue<DivinaQuality>(defaults.quality, DivinaQuality) || DivinaQuality.auto;
|
|
33
|
+
this.scrolled = ensureBoolean(defaults.scrolled) ?? null;
|
|
34
|
+
this.spreads = ensureBoolean(defaults.spreads) ?? true;
|
|
35
|
+
this.stripWidth = ensureValueInRange(defaults.stripWidth, stripWidthRangeConfig.range) || 720;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ConfigurablePreferences } from "../../preferences/Configurable.ts";
|
|
2
|
+
import { RangeConfig } from "../../preferences/Types.ts";
|
|
3
|
+
import { DivinaQuality } from "../DivinaVariantSelector.ts";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ensureBoolean,
|
|
7
|
+
ensureEnumValue,
|
|
8
|
+
ensureNonNegative,
|
|
9
|
+
ensureString,
|
|
10
|
+
ensureValueInRange
|
|
11
|
+
} from "../../preferences/guards.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maximum width in pixels of the vertical strip in scrolled mode.
|
|
15
|
+
*/
|
|
16
|
+
export const stripWidthRangeConfig: RangeConfig = {
|
|
17
|
+
range: [480, 2400],
|
|
18
|
+
step: 20
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IDivinaPreferences {
|
|
22
|
+
/** Background color behind the pages (equivalent of the EPUB page background theme) */
|
|
23
|
+
backgroundColor?: string | null,
|
|
24
|
+
/** Number of pixels to constrain the container width by (e.g. for docked panels) */
|
|
25
|
+
constraint?: number | null,
|
|
26
|
+
/** Image quality when the publication offers alternate resolutions */
|
|
27
|
+
quality?: DivinaQuality | null,
|
|
28
|
+
/** Display the publication as a vertical scroll instead of horizontal pages */
|
|
29
|
+
scrolled?: boolean | null,
|
|
30
|
+
/** Display two pages side-by-side on landscape viewports (paged mode only) */
|
|
31
|
+
spreads?: boolean | null,
|
|
32
|
+
/** Maximum width in pixels of the vertical strip in scrolled mode */
|
|
33
|
+
stripWidth?: number | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class DivinaPreferences implements ConfigurablePreferences<DivinaPreferences> {
|
|
37
|
+
backgroundColor?: string | null;
|
|
38
|
+
constraint?: number | null;
|
|
39
|
+
quality?: DivinaQuality | null;
|
|
40
|
+
scrolled?: boolean | null;
|
|
41
|
+
spreads?: boolean | null;
|
|
42
|
+
stripWidth?: number | null;
|
|
43
|
+
|
|
44
|
+
constructor(preferences: IDivinaPreferences = {}) {
|
|
45
|
+
this.backgroundColor = ensureString(preferences.backgroundColor);
|
|
46
|
+
this.constraint = ensureNonNegative(preferences.constraint);
|
|
47
|
+
this.quality = ensureEnumValue<DivinaQuality>(preferences.quality, DivinaQuality);
|
|
48
|
+
this.scrolled = ensureBoolean(preferences.scrolled);
|
|
49
|
+
this.spreads = ensureBoolean(preferences.spreads);
|
|
50
|
+
this.stripWidth = ensureValueInRange(preferences.stripWidth, stripWidthRangeConfig.range);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static serialize(preferences: DivinaPreferences): string {
|
|
54
|
+
const { ...properties } = preferences;
|
|
55
|
+
return JSON.stringify(properties);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static deserialize(preferences: string): DivinaPreferences | null {
|
|
59
|
+
try {
|
|
60
|
+
const parsedPreferences = JSON.parse(preferences);
|
|
61
|
+
return new DivinaPreferences(parsedPreferences);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("Failed to deserialize preferences:", error);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
merging(other: DivinaPreferences): DivinaPreferences {
|
|
69
|
+
const merged: IDivinaPreferences = { ...this };
|
|
70
|
+
for (const key of Object.keys(other) as (keyof IDivinaPreferences)[]) {
|
|
71
|
+
if (other[key] !== undefined) {
|
|
72
|
+
(merged as Record<string, unknown>)[key] = other[key];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return new DivinaPreferences(merged);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Layout, Metadata } from "@readium/shared";
|
|
2
|
+
|
|
3
|
+
import { IPreferencesEditor } from "../../preferences/PreferencesEditor.ts";
|
|
4
|
+
import { BooleanPreference, EnumPreference, Preference, RangePreference } from "../../preferences/Preference.ts";
|
|
5
|
+
import { DivinaQuality } from "../DivinaVariantSelector.ts";
|
|
6
|
+
import { DivinaPreferences, stripWidthRangeConfig } from "./DivinaPreferences.ts";
|
|
7
|
+
import { DivinaSettings } from "./DivinaSettings.ts";
|
|
8
|
+
|
|
9
|
+
export class DivinaPreferencesEditor implements IPreferencesEditor {
|
|
10
|
+
preferences: DivinaPreferences;
|
|
11
|
+
private settings: DivinaSettings;
|
|
12
|
+
private metadata: Metadata;
|
|
13
|
+
|
|
14
|
+
constructor(initialPreferences: DivinaPreferences, settings: DivinaSettings, metadata: Metadata) {
|
|
15
|
+
this.preferences = new DivinaPreferences({ ...initialPreferences });
|
|
16
|
+
this.settings = settings;
|
|
17
|
+
this.metadata = metadata;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
clear(): void {
|
|
21
|
+
this.preferences = new DivinaPreferences({
|
|
22
|
+
backgroundColor: null,
|
|
23
|
+
constraint: null,
|
|
24
|
+
quality: null,
|
|
25
|
+
scrolled: null,
|
|
26
|
+
spreads: null,
|
|
27
|
+
stripWidth: null
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private updatePreference<K extends keyof DivinaPreferences>(key: K, value: DivinaPreferences[K]) {
|
|
32
|
+
this.preferences[key] = value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private get manifestScrolled(): boolean {
|
|
36
|
+
return this.metadata.effectiveLayout === Layout.scrolled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get backgroundColor(): Preference<string> {
|
|
40
|
+
return new Preference<string>({
|
|
41
|
+
initialValue: this.preferences.backgroundColor,
|
|
42
|
+
effectiveValue: this.settings.backgroundColor,
|
|
43
|
+
isEffective: true,
|
|
44
|
+
onChange: (newValue) => {
|
|
45
|
+
this.updatePreference("backgroundColor", newValue ?? null);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get quality(): EnumPreference<DivinaQuality> {
|
|
51
|
+
return new EnumPreference<DivinaQuality>({
|
|
52
|
+
initialValue: this.preferences.quality,
|
|
53
|
+
effectiveValue: this.settings.quality,
|
|
54
|
+
isEffective: true,
|
|
55
|
+
onChange: (newValue) => {
|
|
56
|
+
this.updatePreference("quality", newValue ?? null);
|
|
57
|
+
},
|
|
58
|
+
supportedValues: [DivinaQuality.auto, DivinaQuality.low, DivinaQuality.high, DivinaQuality.max]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get scrolled(): BooleanPreference {
|
|
63
|
+
return new BooleanPreference({
|
|
64
|
+
initialValue: this.preferences.scrolled,
|
|
65
|
+
effectiveValue: this.settings.scrolled,
|
|
66
|
+
isEffective: !this.manifestScrolled,
|
|
67
|
+
onChange: (newValue) => {
|
|
68
|
+
this.updatePreference("scrolled", newValue ?? null);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get spreads(): BooleanPreference {
|
|
74
|
+
return new BooleanPreference({
|
|
75
|
+
initialValue: this.preferences.spreads,
|
|
76
|
+
effectiveValue: this.settings.spreads,
|
|
77
|
+
isEffective: !this.settings.scrolled,
|
|
78
|
+
onChange: (newValue) => {
|
|
79
|
+
this.updatePreference("spreads", newValue ?? null);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get stripWidth(): RangePreference<number> {
|
|
85
|
+
return new RangePreference<number>({
|
|
86
|
+
initialValue: this.preferences.stripWidth,
|
|
87
|
+
effectiveValue: this.settings.stripWidth,
|
|
88
|
+
isEffective: this.settings.scrolled,
|
|
89
|
+
onChange: (newValue) => {
|
|
90
|
+
this.updatePreference("stripWidth", newValue ?? null);
|
|
91
|
+
},
|
|
92
|
+
supportedRange: stripWidthRangeConfig.range,
|
|
93
|
+
step: stripWidthRangeConfig.step
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ConfigurableSettings } from "../../preferences/Configurable.ts";
|
|
2
|
+
import { DivinaQuality } from "../DivinaVariantSelector.ts";
|
|
3
|
+
import { DivinaDefaults } from "./DivinaDefaults.ts";
|
|
4
|
+
import { DivinaPreferences } from "./DivinaPreferences.ts";
|
|
5
|
+
|
|
6
|
+
export interface IDivinaSettings {
|
|
7
|
+
backgroundColor?: string | null,
|
|
8
|
+
constraint?: number | null,
|
|
9
|
+
quality?: DivinaQuality | null,
|
|
10
|
+
scrolled?: boolean | null,
|
|
11
|
+
spreads?: boolean | null,
|
|
12
|
+
stripWidth?: number | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DivinaSettings implements ConfigurableSettings {
|
|
16
|
+
backgroundColor: string | null;
|
|
17
|
+
constraint: number;
|
|
18
|
+
quality: DivinaQuality;
|
|
19
|
+
scrolled: boolean;
|
|
20
|
+
spreads: boolean;
|
|
21
|
+
stripWidth: number;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param manifestScrolled Whether the publication itself declares `layout: scrolled`.
|
|
25
|
+
* A natively scrolled publication (e.g. webtoon) cannot be switched to paged mode,
|
|
26
|
+
* while a fixed publication can be switched to scrolled by the user.
|
|
27
|
+
*/
|
|
28
|
+
constructor(preferences: DivinaPreferences, defaults: DivinaDefaults, manifestScrolled: boolean) {
|
|
29
|
+
this.backgroundColor = preferences.backgroundColor ?? defaults.backgroundColor;
|
|
30
|
+
this.constraint = preferences.constraint ?? defaults.constraint;
|
|
31
|
+
this.quality = preferences.quality ?? defaults.quality;
|
|
32
|
+
this.scrolled = manifestScrolled
|
|
33
|
+
? true
|
|
34
|
+
: (preferences.scrolled ?? defaults.scrolled ?? false);
|
|
35
|
+
this.spreads = preferences.spreads ?? defaults.spreads;
|
|
36
|
+
this.stripWidth = preferences.stripWidth ?? defaults.stripWidth;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NavigatorProtector } from "../../protection/NavigatorProtector.ts";
|
|
2
|
+
import { DragAndDropProtector } from "../../protection/DragAndDropProtector.ts";
|
|
3
|
+
import { CopyProtector } from "../../protection/CopyProtector.ts";
|
|
4
|
+
import { IContentProtectionConfig } from "../../Navigator.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Content protection for the Divina navigator. Because Divina renders images
|
|
8
|
+
* directly in the host DOM (no iframes), the in-frame halves of the EPUB
|
|
9
|
+
* protection (copy and drag-and-drop blocking) must run host-side instead,
|
|
10
|
+
* like the audio navigator does.
|
|
11
|
+
*/
|
|
12
|
+
export class DivinaNavigatorProtector extends NavigatorProtector {
|
|
13
|
+
private dragAndDropProtector?: DragAndDropProtector;
|
|
14
|
+
private copyProtector?: CopyProtector;
|
|
15
|
+
|
|
16
|
+
constructor(config: IContentProtectionConfig = {}) {
|
|
17
|
+
super(config);
|
|
18
|
+
|
|
19
|
+
if (config.disableDragAndDrop) {
|
|
20
|
+
this.dragAndDropProtector = new DragAndDropProtector({
|
|
21
|
+
onDragDetected: (dataTransferTypes) => {
|
|
22
|
+
this.dispatchSuspiciousActivity("drag_detected", { dataTransferTypes, targetFrameSrc: "" });
|
|
23
|
+
},
|
|
24
|
+
onDropDetected: (dataTransferTypes, fileCount) => {
|
|
25
|
+
this.dispatchSuspiciousActivity("drop_detected", { dataTransferTypes, fileCount, targetFrameSrc: "" });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (config.protectCopy) {
|
|
31
|
+
this.copyProtector = new CopyProtector({
|
|
32
|
+
onCopyBlocked: () => {
|
|
33
|
+
this.dispatchSuspiciousActivity("bulk_copy", { targetFrameSrc: "" });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public override destroy() {
|
|
40
|
+
super.destroy();
|
|
41
|
+
this.dragAndDropProtector?.destroy();
|
|
42
|
+
this.copyProtector?.destroy();
|
|
43
|
+
}
|
|
44
|
+
}
|