@readium/navigator 2.6.1 → 2.7.0-alpha.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.
Files changed (51) hide show
  1. package/LICENSE +28 -0
  2. package/dist/index.js +4795 -3156
  3. package/dist/index.umd.cjs +42 -40
  4. package/package.json +10 -10
  5. package/src/divina/DivinaNavigator.ts +662 -0
  6. package/src/divina/DivinaPageManager.ts +322 -0
  7. package/src/divina/DivinaPagedPresenter.ts +508 -0
  8. package/src/divina/DivinaPeripherals.ts +630 -0
  9. package/src/divina/DivinaScrolledPresenter.ts +516 -0
  10. package/src/divina/DivinaSpreader.ts +202 -0
  11. package/src/divina/DivinaVariantSelector.ts +77 -0
  12. package/src/divina/index.ts +8 -0
  13. package/src/divina/preferences/DivinaDefaults.ts +37 -0
  14. package/src/divina/preferences/DivinaPreferences.ts +77 -0
  15. package/src/divina/preferences/DivinaPreferencesEditor.ts +89 -0
  16. package/src/divina/preferences/DivinaSettings.ts +38 -0
  17. package/src/divina/preferences/index.ts +4 -0
  18. package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
  19. package/src/epub/EpubNavigator.ts +59 -3
  20. package/src/epub/frame/FrameBlobBuilder.ts +85 -32
  21. package/src/epub/frame/FrameManager.ts +3 -3
  22. package/src/epub/frame/FramePoolManager.ts +78 -34
  23. package/src/epub/fxl/FXLFrameManager.ts +69 -26
  24. package/src/epub/fxl/FXLFramePoolManager.ts +88 -50
  25. package/src/epub/fxl/FXLPeripherals.ts +22 -1
  26. package/src/index.ts +1 -0
  27. package/src/peripherals/KeyboardConditionBridge.ts +15 -5
  28. package/src/peripherals/KeyboardPeripherals.ts +13 -7
  29. package/src/webpub/WebPubFrameManager.ts +3 -3
  30. package/types/src/divina/DivinaNavigator.d.ts +113 -0
  31. package/types/src/divina/DivinaPageManager.d.ts +66 -0
  32. package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
  33. package/types/src/divina/DivinaPeripherals.d.ts +121 -0
  34. package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
  35. package/types/src/divina/DivinaSpreader.d.ts +65 -0
  36. package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
  37. package/types/src/divina/index.d.ts +8 -0
  38. package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
  39. package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
  40. package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
  41. package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
  42. package/types/src/divina/preferences/index.d.ts +4 -0
  43. package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
  44. package/types/src/epub/EpubNavigator.d.ts +3 -1
  45. package/types/src/epub/frame/FrameBlobBuilder.d.ts +6 -3
  46. package/types/src/epub/frame/FramePoolManager.d.ts +1 -0
  47. package/types/src/epub/fxl/FXLFrameManager.d.ts +2 -0
  48. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +6 -2
  49. package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
  50. package/types/src/index.d.ts +1 -0
  51. package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -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/navigator-html-injectables";
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) || 1000;
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,89 @@
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 = initialPreferences;
16
+ this.settings = settings;
17
+ this.metadata = metadata;
18
+ }
19
+
20
+ clear(): void {
21
+ this.preferences = new DivinaPreferences({});
22
+ }
23
+
24
+ private updatePreference<K extends keyof DivinaPreferences>(key: K, value: DivinaPreferences[K]) {
25
+ this.preferences[key] = value;
26
+ }
27
+
28
+ private get manifestScrolled(): boolean {
29
+ return this.metadata.effectiveLayout === Layout.scrolled;
30
+ }
31
+
32
+ get backgroundColor(): Preference<string> {
33
+ return new Preference<string>({
34
+ initialValue: this.preferences.backgroundColor,
35
+ effectiveValue: this.settings.backgroundColor,
36
+ isEffective: true,
37
+ onChange: (newValue) => {
38
+ this.updatePreference("backgroundColor", newValue ?? null);
39
+ }
40
+ });
41
+ }
42
+
43
+ get quality(): EnumPreference<DivinaQuality> {
44
+ return new EnumPreference<DivinaQuality>({
45
+ initialValue: this.preferences.quality,
46
+ effectiveValue: this.settings.quality,
47
+ isEffective: true,
48
+ onChange: (newValue) => {
49
+ this.updatePreference("quality", newValue ?? null);
50
+ },
51
+ supportedValues: [DivinaQuality.auto, DivinaQuality.low, DivinaQuality.high, DivinaQuality.max]
52
+ });
53
+ }
54
+
55
+ get scrolled(): BooleanPreference {
56
+ return new BooleanPreference({
57
+ initialValue: this.preferences.scrolled,
58
+ effectiveValue: this.settings.scrolled,
59
+ isEffective: !this.manifestScrolled,
60
+ onChange: (newValue) => {
61
+ this.updatePreference("scrolled", newValue ?? null);
62
+ }
63
+ });
64
+ }
65
+
66
+ get spreads(): BooleanPreference {
67
+ return new BooleanPreference({
68
+ initialValue: this.preferences.spreads,
69
+ effectiveValue: this.settings.spreads,
70
+ isEffective: !this.settings.scrolled,
71
+ onChange: (newValue) => {
72
+ this.updatePreference("spreads", newValue ?? null);
73
+ }
74
+ });
75
+ }
76
+
77
+ get stripWidth(): RangePreference<number> {
78
+ return new RangePreference<number>({
79
+ initialValue: this.preferences.stripWidth,
80
+ effectiveValue: this.settings.stripWidth,
81
+ isEffective: this.settings.scrolled,
82
+ onChange: (newValue) => {
83
+ this.updatePreference("stripWidth", newValue ?? null);
84
+ },
85
+ supportedRange: stripWidthRangeConfig.range,
86
+ step: stripWidthRangeConfig.step
87
+ });
88
+ }
89
+ }
@@ -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,4 @@
1
+ export * from "./DivinaDefaults.ts";
2
+ export * from "./DivinaPreferences.ts";
3
+ export * from "./DivinaPreferencesEditor.ts";
4
+ export * from "./DivinaSettings.ts";
@@ -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
+ }