@papyrus-sdk/ui-react-native 0.2.24 → 0.2.27

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.
@@ -20482,358 +20482,644 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20482
20482
  </script>
20483
20483
  <!-- @papyrus-comic-runtime:end -->
20484
20484
  <script>
20485
- (function () {
20486
- const viewer = document.getElementById('viewer');
20487
- const DEFAULT_FONT_SIZE = 16;
20488
- const TEXT_PAGE_CHUNK = 1600;
20485
+ (function () {
20486
+ const viewer = document.getElementById('viewer');
20487
+ const DEFAULT_FONT_SIZE = 16;
20488
+ const TEXT_PAGE_CHUNK = 1600;
20489
20489
 
20490
- let currentType = null;
20491
- let book = null;
20492
- let rendition = null;
20493
- let spineItems = [];
20494
- let textPages = [];
20495
- let textContainer = null;
20496
- let currentPage = 1;
20497
- let pageCount = 0;
20498
- let zoom = 1.0;
20499
- let rotation = 0;
20500
- let comicArchive = null;
20501
- let comicEntries = [];
20502
- let comicContainer = null;
20503
- let comicPages = [];
20504
- let comicObserver = null;
20505
- let comicScrollFrame = null;
20506
- const comicObjectUrls = new Map();
20507
- const comicLoading = new Map();
20508
- const comicDimensions = new Map();
20509
- const comicPreviewCache = new Map();
20510
- const comicPreviewLoading = new Map();
20511
- const COMIC_CACHE_LIMIT = 16;
20512
- const COMIC_PREVIEW_CACHE_LIMIT = 8;
20513
- const comicHelpers = window.PapyrusComicRuntime || {};
20514
- const runtimeConfig = window.__PAPYRUS_RUNTIME_CONFIG__ || {};
20515
- let comicDispose = null;
20516
- let comicGeneration = 0;
20490
+ let currentType = null;
20491
+ let book = null;
20492
+ let rendition = null;
20493
+ let spineItems = [];
20494
+ let textPages = [];
20495
+ let textContainer = null;
20496
+ let currentPage = 1;
20497
+ let pageCount = 0;
20498
+ let zoom = 1.0;
20499
+ let rotation = 0;
20500
+ let comicArchive = null;
20501
+ let comicEntries = [];
20502
+ let comicContainer = null;
20503
+ let comicPages = [];
20504
+ let comicObserver = null;
20505
+ let comicScrollFrame = null;
20506
+ const comicVisiblePages = new Set();
20507
+ const comicObjectUrls = new Map();
20508
+ const comicLoading = new Map();
20509
+ const comicDimensions = new Map();
20510
+ const comicPreviewCache = new Map();
20511
+ const comicPreviewLoading = new Map();
20512
+ const COMIC_CACHE_LIMIT = 16;
20513
+ const COMIC_PREVIEW_CACHE_LIMIT = 8;
20514
+ const comicHelpers = window.PapyrusComicRuntime || {};
20515
+ const runtimeConfig = window.__PAPYRUS_RUNTIME_CONFIG__ || {};
20516
+ const epubDiagnosticsEnabled =
20517
+ runtimeConfig.epubDiagnostics === true ||
20518
+ runtimeConfig.epubDiagnostics === 'true';
20519
+ const EPUB_STAGE_TIMEOUT_MS = 10000;
20520
+ let comicDispose = null;
20521
+ let comicGeneration = 0;
20522
+ let epubInteractionCleanup = null;
20523
+ const epubSelectionCleanups = new Set();
20524
+ let epubScrollDiagnostics = null;
20525
+ // Shared generation for all document formats. The historical variable name
20526
+ // is kept because the shipped runtime tests and diagnostics refer to it.
20527
+ let epubLoadGeneration = 0;
20517
20528
 
20518
- const sendMessage = (payload) => {
20519
- if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
20520
- window.ReactNativeWebView.postMessage(JSON.stringify(payload));
20521
- return;
20522
- }
20523
- if (window.parent && window.parent !== window) {
20524
- window.parent.postMessage(payload, '*');
20525
- }
20526
- };
20527
-
20528
- const sendResponse = (id, ok, data, error) => {
20529
- sendMessage({ type: 'response', id, ok, data, error });
20530
- };
20529
+ const sendMessage = (payload) => {
20530
+ if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
20531
+ window.ReactNativeWebView.postMessage(JSON.stringify(payload));
20532
+ return;
20533
+ }
20534
+ if (window.parent && window.parent !== window) {
20535
+ window.parent.postMessage(payload, '*');
20536
+ }
20537
+ };
20531
20538
 
20532
- const runtimeAssetRequests = new Map();
20533
- const fileChunkRequests = new Map();
20539
+ const sendResponse = (id, ok, data, error) => {
20540
+ sendMessage({ type: 'response', id, ok, data, error });
20541
+ };
20534
20542
 
20535
- const requestRuntimeAsset = (url, encoding) =>
20536
- new Promise((resolve, reject) => {
20537
- const id = `runtime-asset-${Date.now()}-${Math.random()}`;
20538
- const timeout = setTimeout(() => {
20539
- runtimeAssetRequests.delete(id);
20540
- reject(new Error(`Tempo esgotado ao carregar asset CBR: ${url}`));
20541
- }, 15000);
20542
- runtimeAssetRequests.set(id, {resolve, reject, timeout});
20543
- sendMessage({type: 'asset-request', id, url, encoding});
20544
- });
20543
+ const runtimeAssetRequests = new Map();
20544
+ const fileChunkRequests = new Map();
20545
20545
 
20546
- const encodeArrayBufferAsDataUrl = async (buffer) => {
20547
- const bytes = new Uint8Array(buffer);
20548
- let binary = '';
20549
- const chunkSize = 0x8000;
20550
- for (let offset = 0; offset < bytes.length; offset += chunkSize) {
20551
- binary += String.fromCharCode(
20552
- ...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),
20553
- );
20554
- }
20555
- return `data:application/wasm;base64,${btoa(binary)}`;
20556
- };
20546
+ const requestRuntimeAsset = (url, encoding) =>
20547
+ new Promise((resolve, reject) => {
20548
+ const id = `runtime-asset-${Date.now()}-${Math.random()}`;
20549
+ const timeout = setTimeout(() => {
20550
+ runtimeAssetRequests.delete(id);
20551
+ reject(new Error(`Tempo esgotado ao carregar asset CBR: ${url}`));
20552
+ }, 15000);
20553
+ runtimeAssetRequests.set(id, {resolve, reject, timeout});
20554
+ sendMessage({type: 'asset-request', id, url, encoding});
20555
+ });
20557
20556
 
20558
- const fetchRuntimeAsset = async (url, encoding) => {
20559
- try {
20560
- const response = await fetch(url);
20561
- if (!response.ok) throw new Error(`status ${response.status}`);
20562
- if (encoding === 'text') return await response.text();
20563
- return await encodeArrayBufferAsDataUrl(await response.arrayBuffer());
20564
- } catch {
20565
- return await requestRuntimeAsset(url, encoding);
20566
- }
20567
- };
20557
+ const encodeArrayBufferAsDataUrl = async (buffer) => {
20558
+ const bytes = new Uint8Array(buffer);
20559
+ let binary = '';
20560
+ const chunkSize = 0x8000;
20561
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
20562
+ binary += String.fromCharCode(
20563
+ ...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),
20564
+ );
20565
+ }
20566
+ return `data:application/wasm;base64,${btoa(binary)}`;
20567
+ };
20568
20568
 
20569
- const requestLocalFileChunk = (uri, offset, length) =>
20570
- new Promise((resolve, reject) => {
20571
- const id = `file-chunk-${Date.now()}-${Math.random()}`;
20572
- const timeout = setTimeout(() => {
20573
- fileChunkRequests.delete(id);
20574
- reject(new Error(`Tempo esgotado ao ler arquivo local: ${uri}`));
20575
- }, 30000);
20576
- fileChunkRequests.set(id, {resolve, reject, timeout});
20577
- sendMessage({type: 'file-chunk-request', id, uri, offset, length});
20578
- });
20569
+ const fetchRuntimeAsset = async (url, encoding) => {
20570
+ try {
20571
+ const response = await fetch(url);
20572
+ if (!response.ok) throw new Error(`status ${response.status}`);
20573
+ if (encoding === 'text') return await response.text();
20574
+ return await encodeArrayBufferAsDataUrl(await response.arrayBuffer());
20575
+ } catch {
20576
+ return await requestRuntimeAsset(url, encoding);
20577
+ }
20578
+ };
20579
20579
 
20580
- const readLocalFile = async (uri) => {
20581
- const chunkSize = 1024 * 1024;
20582
- const chunks = [];
20583
- let offset = 0;
20584
- let totalLength = 0;
20585
- while (true) {
20586
- const result = await requestLocalFileChunk(uri, offset, chunkSize);
20587
- const bytes = decodeBase64(result.data || '');
20588
- if (bytes.length === 0 && !result.done) {
20589
- throw new Error('Leitura nativa retornou um chunk vazio.');
20590
- }
20591
- chunks.push(bytes);
20592
- totalLength += bytes.length;
20593
- offset += bytes.length;
20594
- if (result.done) break;
20595
- }
20580
+ const requestLocalFileChunk = (uri, offset, length) =>
20581
+ new Promise((resolve, reject) => {
20582
+ const id = `file-chunk-${Date.now()}-${Math.random()}`;
20583
+ const timeout = setTimeout(() => {
20584
+ fileChunkRequests.delete(id);
20585
+ reject(new Error(`Tempo esgotado ao ler arquivo local: ${uri}`));
20586
+ }, 30000);
20587
+ fileChunkRequests.set(id, {resolve, reject, timeout});
20588
+ sendMessage({type: 'file-chunk-request', id, uri, offset, length});
20589
+ });
20596
20590
 
20597
- const output = new Uint8Array(totalLength);
20598
- let outputOffset = 0;
20599
- chunks.forEach((chunk) => {
20600
- output.set(chunk, outputOffset);
20601
- outputOffset += chunk.length;
20602
- });
20603
- return output.buffer;
20604
- };
20591
+ const readLocalFile = async (uri) => {
20592
+ const chunkSize = 1024 * 1024;
20593
+ const chunks = [];
20594
+ let offset = 0;
20595
+ let totalLength = 0;
20596
+ while (true) {
20597
+ const result = await requestLocalFileChunk(uri, offset, chunkSize);
20598
+ const bytes = decodeBase64(result.data || '');
20599
+ if (bytes.length === 0 && !result.done) {
20600
+ throw new Error('Leitura nativa retornou um chunk vazio.');
20601
+ }
20602
+ chunks.push(bytes);
20603
+ totalLength += bytes.length;
20604
+ offset += bytes.length;
20605
+ if (result.done) break;
20606
+ }
20605
20607
 
20606
- const sendState = (extra) => {
20607
- sendMessage({
20608
- type: 'state',
20609
- payload: {
20610
- pageCount,
20611
- currentPage,
20612
- zoom,
20613
- ...(extra || {}),
20614
- },
20615
- });
20616
- };
20608
+ const output = new Uint8Array(totalLength);
20609
+ let outputOffset = 0;
20610
+ chunks.forEach((chunk) => {
20611
+ output.set(chunk, outputOffset);
20612
+ outputOffset += chunk.length;
20613
+ });
20614
+ return output.buffer;
20615
+ };
20617
20616
 
20618
- const sendEvent = (name, payload) => {
20619
- sendMessage({ type: 'event', name, payload });
20620
- };
20617
+ const sendState = (extra) => {
20618
+ sendMessage({
20619
+ type: 'state',
20620
+ payload: {
20621
+ pageCount,
20622
+ currentPage,
20623
+ zoom,
20624
+ ...(extra || {}),
20625
+ },
20626
+ });
20627
+ };
20621
20628
 
20622
- const reportError = (error, context) => {
20623
- const message = error && error.message ? error.message : String(error);
20624
- const stack = error && error.stack ? error.stack : null;
20625
- sendEvent('RUNTIME_ERROR', { message, context, stack });
20626
- };
20629
+ const sendEvent = (name, payload) => {
20630
+ sendMessage({ type: 'event', name, payload });
20631
+ };
20627
20632
 
20628
- const shouldIgnoreError = (error) => {
20629
- const message = error && error.message ? error.message : String(error || '');
20630
- return message.includes('ResizeObserver loop');
20631
- };
20633
+ const sendEpubDiagnostic = (name, payload) => {
20634
+ if (!epubDiagnosticsEnabled) return;
20635
+ sendEvent(name, {
20636
+ loadId: payload.loadId,
20637
+ timestamp: Date.now(),
20638
+ sourceKind: payload.sourceKind,
20639
+ byteLength: payload.byteLength,
20640
+ currentType: currentType,
20641
+ durationMs: payload.durationMs,
20642
+ uri: payload.uri,
20643
+ status: payload.status,
20644
+ contentLength: payload.contentLength,
20645
+ contentType: payload.contentType,
20646
+ transferEncoding: payload.transferEncoding,
20647
+ error: payload.error,
20648
+ });
20649
+ };
20632
20650
 
20633
- window.addEventListener('error', (event) => {
20634
- const error = event.error || event.message;
20635
- if (shouldIgnoreError(error)) return;
20636
- reportError(error, 'window.error');
20651
+ const withEpubStageTimeout = (promise, stage, payload) =>
20652
+ new Promise((resolve, reject) => {
20653
+ let settled = false;
20654
+ const startedAt = performance.now();
20655
+ const timeout = setTimeout(() => {
20656
+ if (settled) return;
20657
+ settled = true;
20658
+ const error = new Error(`EPUB ${stage} excedeu ${EPUB_STAGE_TIMEOUT_MS}ms`);
20659
+ sendEpubDiagnostic('epub.load.timeout', {
20660
+ ...payload,
20661
+ error: { message: error.message, stage },
20662
+ durationMs: performance.now() - startedAt,
20637
20663
  });
20664
+ reject(error);
20665
+ }, EPUB_STAGE_TIMEOUT_MS);
20638
20666
 
20639
- window.addEventListener('unhandledrejection', (event) => {
20640
- if (shouldIgnoreError(event.reason)) return;
20641
- reportError(event.reason, 'unhandledrejection');
20642
- });
20667
+ Promise.resolve(promise).then(
20668
+ (value) => {
20669
+ if (settled) return;
20670
+ settled = true;
20671
+ clearTimeout(timeout);
20672
+ resolve(value);
20673
+ },
20674
+ (error) => {
20675
+ if (settled) return;
20676
+ settled = true;
20677
+ clearTimeout(timeout);
20678
+ reject(error);
20679
+ },
20680
+ );
20681
+ });
20643
20682
 
20644
- const clearComicState = () => {
20645
- comicGeneration += 1;
20646
- if (comicObserver) {
20647
- comicObserver.disconnect();
20648
- comicObserver = null;
20649
- }
20650
- if (comicScrollFrame !== null) {
20651
- cancelAnimationFrame(comicScrollFrame);
20652
- comicScrollFrame = null;
20653
- }
20654
- comicObjectUrls.forEach((url) => URL.revokeObjectURL(url));
20655
- comicObjectUrls.clear();
20656
- comicLoading.clear();
20657
- comicDimensions.clear();
20658
- comicPreviewCache.clear();
20659
- comicPreviewLoading.clear();
20660
- if (comicDispose) {
20661
- void comicDispose();
20662
- comicDispose = null;
20663
- }
20664
- comicArchive = null;
20665
- comicEntries = [];
20666
- comicContainer = null;
20667
- comicPages = [];
20668
- };
20683
+ const isEpubDiagnosticsEnabled = () => {
20684
+ const configured = runtimeConfig.epubScrollDiagnostics;
20685
+ return configured === true || configured === '1' || configured === 'true' ||
20686
+ Boolean(window.__PAPYRUS_EPUB_SCROLL_DIAGNOSTICS__);
20687
+ };
20669
20688
 
20670
- const clearViewer = () => {
20671
- clearComicState();
20672
- while (viewer.firstChild) {
20673
- viewer.removeChild(viewer.firstChild);
20674
- }
20675
- };
20689
+ const createEpubScrollDiagnostics = (manager) => {
20690
+ if (!isEpubDiagnosticsEnabled()) return null;
20676
20691
 
20677
- const decodeBase64 = (value) => {
20678
- const clean = value.replace(/\s/g, '');
20679
- const binary = atob(clean);
20680
- const len = binary.length;
20681
- const bytes = new Uint8Array(len);
20682
- for (let i = 0; i < len; i += 1) {
20683
- bytes[i] = binary.charCodeAt(i);
20684
- }
20685
- return bytes;
20686
- };
20692
+ const state = {
20693
+ lastScrollTop: null,
20694
+ lastTouchY: null,
20695
+ lastTouchX: null,
20696
+ lastInputDeltaY: 0,
20697
+ touchActive: false,
20698
+ selectionActive: false,
20699
+ gestureLock: false,
20700
+ consecutiveStallSamples: 0,
20701
+ firstStallTimestamp: null,
20702
+ stallReported: false,
20703
+ };
20704
+ const container = manager && manager.container;
20687
20705
 
20688
- const decodeBase64ToText = (value) => {
20689
- const bytes = decodeBase64(value);
20690
- return new TextDecoder('utf-8').decode(bytes);
20691
- };
20706
+ const getSnapshot = () => {
20707
+ const scrollTop = Number(container && container.scrollTop) || 0;
20708
+ const scrollHeight = Number(container && container.scrollHeight) || 0;
20709
+ const clientHeight = Number(container && container.clientHeight) || 0;
20710
+ let scrollEnabled = true;
20711
+ try {
20712
+ const overflowY = window.getComputedStyle(container).overflowY;
20713
+ scrollEnabled = overflowY !== 'hidden' && overflowY !== 'clip';
20714
+ } catch (_) {}
20715
+ return { scrollTop, scrollHeight, clientHeight, scrollEnabled };
20716
+ };
20692
20717
 
20693
- const sourceToArrayBuffer = async (source) => {
20694
- if (source.kind === 'uri') {
20695
- if (/^(?:file|content):\/\//i.test(source.uri)) {
20696
- return readLocalFile(source.uri);
20697
- }
20698
- const response = await fetch(source.uri);
20699
- if (!response.ok && response.status !== 0) {
20700
- throw new Error(`Falha ao carregar quadrinho (${response.status})`);
20701
- }
20702
- return response.arrayBuffer();
20703
- }
20704
- if (source.kind === 'base64') {
20705
- return decodeBase64(source.data).buffer;
20706
- }
20707
- throw new Error('Fonte de quadrinho inválida.');
20708
- };
20718
+ const emit = (name, payload) => {
20719
+ sendEvent(name, {
20720
+ timestamp: Date.now(),
20721
+ documentType: currentType,
20722
+ currentPage,
20723
+ spineIndex: Math.max(0, currentPage - 1),
20724
+ progress: pageCount > 0 ? currentPage / pageCount : null,
20725
+ ...(payload || {}),
20726
+ });
20727
+ };
20709
20728
 
20710
- const comicMimeType = (name) => {
20711
- const extension = name.split('.').pop()?.toLowerCase();
20712
- if (extension === 'png') return 'image/png';
20713
- if (extension === 'gif') return 'image/gif';
20714
- if (extension === 'svg') return 'image/svg+xml';
20715
- if (extension === 'webp') return 'image/webp';
20716
- return 'image/jpeg';
20717
- };
20729
+ const resetStall = () => {
20730
+ state.consecutiveStallSamples = 0;
20731
+ state.firstStallTimestamp = null;
20732
+ state.stallReported = false;
20733
+ };
20718
20734
 
20719
- const updateComicCurrentPage = () => {
20720
- if (!comicContainer || comicPages.length === 0) return;
20721
- const viewportTop = comicContainer.scrollTop;
20722
- let closestIndex = currentPage - 1;
20723
- let closestDistance = Number.POSITIVE_INFINITY;
20724
- comicPages.forEach((page, index) => {
20725
- const distance = Math.abs(page.offsetTop - viewportTop);
20726
- if (distance < closestDistance) {
20727
- closestDistance = distance;
20728
- closestIndex = index;
20729
- }
20730
- });
20731
- const nextPage = Math.max(1, Math.min(pageCount, closestIndex + 1));
20732
- if (nextPage !== currentPage) {
20733
- currentPage = nextPage;
20734
- sendState();
20735
- }
20736
- };
20735
+ const recordScroll = (event) => {
20736
+ const snapshot = getSnapshot();
20737
+ const previousScrollTop = state.lastScrollTop === null
20738
+ ? snapshot.scrollTop
20739
+ : state.lastScrollTop;
20740
+ const deltaY = state.lastInputDeltaY;
20741
+ const inputDirection = deltaY > 4 ? 'up' : deltaY < -4 ? 'down' : 'none';
20742
+ const offsetUnchanged = Math.abs(snapshot.scrollTop - previousScrollTop) <= 1;
20743
+ const validStallSample = inputDirection === 'up' &&
20744
+ snapshot.scrollTop > 2 &&
20745
+ snapshot.scrollEnabled &&
20746
+ !state.selectionActive &&
20747
+ !state.gestureLock &&
20748
+ offsetUnchanged;
20737
20749
 
20738
- const scheduleComicCurrentPage = () => {
20739
- if (comicScrollFrame !== null) return;
20740
- comicScrollFrame = requestAnimationFrame(() => {
20741
- comicScrollFrame = null;
20742
- updateComicCurrentPage();
20750
+ if (validStallSample) {
20751
+ state.consecutiveStallSamples += 1;
20752
+ if (state.firstStallTimestamp === null) {
20753
+ state.firstStallTimestamp = Date.now();
20754
+ }
20755
+ const durationMs = Date.now() - state.firstStallTimestamp;
20756
+ if (!state.stallReported && state.consecutiveStallSamples >= 3 && durationMs >= 150) {
20757
+ state.stallReported = true;
20758
+ emit('epub.scroll.stall', {
20759
+ ...snapshot,
20760
+ previousScrollTop,
20761
+ deltaY,
20762
+ direction: 'up',
20763
+ consecutiveSamples: state.consecutiveStallSamples,
20764
+ durationMs,
20765
+ selectionActive: state.selectionActive,
20766
+ gestureLock: state.gestureLock,
20743
20767
  });
20744
- };
20768
+ }
20769
+ } else if (!offsetUnchanged || inputDirection !== 'up') {
20770
+ resetStall();
20771
+ }
20745
20772
 
20746
- const evictComicUrls = () => {
20747
- if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) return;
20748
- const protectedIndexes = new Set([
20749
- currentPage - 1,
20750
- currentPage,
20751
- currentPage - 2,
20752
- ]);
20753
- for (const [index, url] of comicObjectUrls) {
20754
- if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) break;
20755
- if (protectedIndexes.has(index)) continue;
20756
- URL.revokeObjectURL(url);
20757
- comicObjectUrls.delete(index);
20758
- const image = comicPages[index]?.querySelector('img');
20759
- const page = comicPages[index];
20760
- const dimensions = comicDimensions.get(index);
20761
- const width = dimensions?.width || image?.naturalWidth || 0;
20762
- const height = dimensions?.height || image?.naturalHeight || 0;
20763
- if (page && width > 0 && height > 0) {
20764
- page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
20765
- width,
20766
- height
20767
- );
20768
- }
20769
- if (image) image.removeAttribute('src');
20770
- }
20771
- };
20773
+ state.lastScrollTop = snapshot.scrollTop;
20774
+ emit('epub.scroll', {
20775
+ ...snapshot,
20776
+ previousScrollTop,
20777
+ deltaY,
20778
+ direction: snapshot.scrollTop < previousScrollTop ? 'up' :
20779
+ snapshot.scrollTop > previousScrollTop ? 'down' : 'none',
20780
+ touchActive: state.touchActive,
20781
+ selectionActive: state.selectionActive,
20782
+ gestureLock: state.gestureLock,
20783
+ });
20784
+ };
20772
20785
 
20773
- const ensureComicPage = async (pageIndex) => {
20774
- if (comicObjectUrls.has(pageIndex)) {
20775
- const image = comicPages[pageIndex]?.querySelector('img');
20776
- if (image && !image.src) image.src = comicObjectUrls.get(pageIndex);
20777
- return comicObjectUrls.get(pageIndex);
20778
- }
20779
- if (comicLoading.has(pageIndex)) return comicLoading.get(pageIndex);
20786
+ return {
20787
+ setSelectionActive(active) {
20788
+ state.selectionActive = Boolean(active);
20789
+ emit('epub.selection', { selectionActive: state.selectionActive });
20790
+ },
20791
+ setGestureLock(active) {
20792
+ state.gestureLock = Boolean(active);
20793
+ emit('epub.gesture-lock', { gestureLock: state.gestureLock });
20794
+ },
20795
+ touchStart(event) {
20796
+ const touch = event && event.touches && event.touches[0];
20797
+ state.touchActive = true;
20798
+ state.lastTouchY = touch ? Number(touch.clientY) : null;
20799
+ state.lastTouchX = touch ? Number(touch.clientX) : null;
20800
+ state.lastInputDeltaY = 0;
20801
+ resetStall();
20802
+ emit('epub.touch.start', { x: state.lastTouchX, y: state.lastTouchY });
20803
+ },
20804
+ touchMove(event) {
20805
+ const touch = event && event.touches && event.touches[0];
20806
+ if (!touch) return;
20807
+ const previousY = state.lastTouchY;
20808
+ const previousX = state.lastTouchX;
20809
+ state.lastTouchY = Number(touch.clientY);
20810
+ state.lastTouchX = Number(touch.clientX);
20811
+ state.lastInputDeltaY = previousY === null ? 0 : previousY - state.lastTouchY;
20812
+ emit('epub.touch.move', {
20813
+ x: state.lastTouchX,
20814
+ y: state.lastTouchY,
20815
+ deltaY: previousY === null ? 0 : previousY - state.lastTouchY,
20816
+ deltaX: previousX === null ? 0 : previousX - state.lastTouchX,
20817
+ });
20818
+ recordScroll();
20819
+ },
20820
+ touchEnd(event) {
20821
+ const touch = event && event.changedTouches && event.changedTouches[0];
20822
+ state.touchActive = false;
20823
+ emit('epub.touch.end', {
20824
+ x: touch ? Number(touch.clientX) : state.lastTouchX,
20825
+ y: touch ? Number(touch.clientY) : state.lastTouchY,
20826
+ });
20827
+ state.lastTouchY = null;
20828
+ state.lastTouchX = null;
20829
+ state.lastInputDeltaY = 0;
20830
+ resetStall();
20831
+ },
20832
+ scroll: recordScroll,
20833
+ relocated(location) {
20834
+ emit('epub.relocated', {
20835
+ href: location && location.start ? location.start.href || null : null,
20836
+ cfi: location && location.start ? location.start.cfi || null : null,
20837
+ });
20838
+ },
20839
+ settled(event) {
20840
+ emit('epub.momentum.settled', {
20841
+ scrollTop: Number(event && event.top) || 0,
20842
+ });
20843
+ },
20844
+ command(name, payload) {
20845
+ emit('epub.programmatic-scroll', { command: name, ...(payload || {}) });
20846
+ },
20847
+ };
20848
+ };
20780
20849
 
20781
- const entry = comicEntries[pageIndex];
20782
- if (!entry) throw new Error(`Página de quadrinho inválida: ${pageIndex}`);
20783
- const loadGeneration = comicGeneration;
20784
- const pending = entry.async('blob').then((blob) => {
20785
- if (
20786
- !comicHelpers.isCurrentComicPageLoad(
20787
- loadGeneration,
20788
- comicGeneration,
20789
- entry,
20790
- comicEntries[pageIndex]
20791
- )
20792
- ) {
20793
- return null;
20794
- }
20795
- const url = URL.createObjectURL(
20796
- new Blob([blob], { type: comicMimeType(entry.name) })
20797
- );
20798
- comicObjectUrls.set(pageIndex, url);
20799
- const image = comicPages[pageIndex]?.querySelector('img');
20800
- if (image) image.src = url;
20801
- evictComicUrls();
20802
- return url;
20803
- });
20804
- comicLoading.set(pageIndex, pending);
20805
- try {
20806
- return await pending;
20807
- } finally {
20808
- if (comicLoading.get(pageIndex) === pending) {
20809
- comicLoading.delete(pageIndex);
20810
- }
20811
- }
20812
- };
20850
+ const reportError = (error, context) => {
20851
+ const message = error && error.message ? error.message : String(error);
20852
+ const stack = error && error.stack ? error.stack : null;
20853
+ sendEvent('RUNTIME_ERROR', { message, context, stack });
20854
+ };
20813
20855
 
20814
- const renderComicPage = (pageIndex) => {
20815
- const image = comicPages[pageIndex]?.querySelector('img');
20816
- if (!image) return;
20817
- image.style.width = `${Math.max(40, Math.round(100 * zoom))}%`;
20818
- image.style.transform = `rotate(${rotation}deg)`;
20819
- };
20856
+ const shouldIgnoreError = (error) => {
20857
+ const message = error && error.message ? error.message : String(error || '');
20858
+ return message.includes('ResizeObserver loop');
20859
+ };
20820
20860
 
20821
- const applyComicLayout = () => {
20822
- comicPages.forEach((_, pageIndex) => renderComicPage(pageIndex));
20823
- };
20861
+ window.addEventListener('error', (event) => {
20862
+ const error = event.error || event.message;
20863
+ if (shouldIgnoreError(error)) return;
20864
+ reportError(error, 'window.error');
20865
+ });
20824
20866
 
20825
- const loadComic = async (source, format) => {
20826
- clearViewer();
20827
- let entries;
20828
- if (format === 'cbr') {
20829
- const clientUrl = runtimeConfig.cbrClientUrl;
20830
- const workerUrl = runtimeConfig.cbrWorkerUrl;
20831
- const wasmUrl = runtimeConfig.cbrWasmUrl;
20832
- if (!clientUrl || !workerUrl || !wasmUrl) {
20833
- throw new Error(
20834
- 'CBR no mobile exige o pacote opcional de runtime libarchive.'
20835
- );
20836
- }
20867
+ window.addEventListener('unhandledrejection', (event) => {
20868
+ if (shouldIgnoreError(event.reason)) return;
20869
+ reportError(event.reason, 'unhandledrejection');
20870
+ });
20871
+
20872
+ const clearComicState = () => {
20873
+ comicGeneration += 1;
20874
+ if (comicObserver) {
20875
+ comicObserver.disconnect();
20876
+ comicObserver = null;
20877
+ }
20878
+ if (comicScrollFrame !== null) {
20879
+ cancelAnimationFrame(comicScrollFrame);
20880
+ comicScrollFrame = null;
20881
+ }
20882
+ comicObjectUrls.forEach((url) => URL.revokeObjectURL(url));
20883
+ comicObjectUrls.clear();
20884
+ comicLoading.clear();
20885
+ comicVisiblePages.clear();
20886
+ comicDimensions.clear();
20887
+ comicPreviewCache.clear();
20888
+ comicPreviewLoading.clear();
20889
+ if (comicDispose) {
20890
+ void comicDispose();
20891
+ comicDispose = null;
20892
+ }
20893
+ comicArchive = null;
20894
+ comicEntries = [];
20895
+ comicContainer = null;
20896
+ comicPages = [];
20897
+ };
20898
+
20899
+ const clearViewer = () => {
20900
+ if (epubInteractionCleanup) {
20901
+ epubInteractionCleanup();
20902
+ epubInteractionCleanup = null;
20903
+ }
20904
+ epubSelectionCleanups.forEach((cleanup) => cleanup());
20905
+ epubSelectionCleanups.clear();
20906
+ clearComicState();
20907
+ while (viewer.firstChild) {
20908
+ viewer.removeChild(viewer.firstChild);
20909
+ }
20910
+ };
20911
+
20912
+ const decodeBase64 = (value) => {
20913
+ const clean = value.replace(/\s/g, '');
20914
+ const binary = atob(clean);
20915
+ const len = binary.length;
20916
+ const bytes = new Uint8Array(len);
20917
+ for (let i = 0; i < len; i += 1) {
20918
+ bytes[i] = binary.charCodeAt(i);
20919
+ }
20920
+ return bytes;
20921
+ };
20922
+
20923
+ const toExactArrayBuffer = (bytes) => {
20924
+ if (
20925
+ bytes.byteOffset === 0 &&
20926
+ bytes.byteLength === bytes.buffer.byteLength
20927
+ ) {
20928
+ return bytes.buffer;
20929
+ }
20930
+ return bytes.buffer.slice(
20931
+ bytes.byteOffset,
20932
+ bytes.byteOffset + bytes.byteLength
20933
+ );
20934
+ };
20935
+
20936
+ const decodeBase64ToText = (value) => {
20937
+ const bytes = decodeBase64(value);
20938
+ return new TextDecoder('utf-8').decode(bytes);
20939
+ };
20940
+
20941
+ const isMetroAssetUri = (value) => {
20942
+ try {
20943
+ const url = new URL(value);
20944
+ return (
20945
+ (url.protocol === 'http:' || url.protocol === 'https:') &&
20946
+ url.pathname.startsWith('/assets/') &&
20947
+ url.searchParams.has('platform') &&
20948
+ url.searchParams.has('hash')
20949
+ );
20950
+ } catch {
20951
+ return false;
20952
+ }
20953
+ };
20954
+
20955
+ const sourceToArrayBuffer = async (source) => {
20956
+ if (source.kind === 'uri') {
20957
+ if (/^(?:file|content|android\.resource):\/\//i.test(source.uri)) {
20958
+ return readLocalFile(source.uri);
20959
+ }
20960
+ const isEpubHttpSource = isMetroAssetUri(source.uri);
20961
+ if (isEpubHttpSource) {
20962
+ sendEpubDiagnostic('epub.source.fetch.start', {uri: source.uri});
20963
+ }
20964
+ const response = await fetch(source.uri);
20965
+ if (isEpubHttpSource) {
20966
+ sendEpubDiagnostic('epub.source.fetch.response', {
20967
+ uri: source.uri,
20968
+ status: response.status,
20969
+ contentLength: response.headers.get('content-length'),
20970
+ contentType: response.headers.get('content-type'),
20971
+ transferEncoding: response.headers.get('transfer-encoding'),
20972
+ });
20973
+ }
20974
+ if (!response.ok && response.status !== 0) {
20975
+ throw new Error(`Falha ao carregar quadrinho (${response.status})`);
20976
+ }
20977
+ if (isEpubHttpSource) {
20978
+ sendEpubDiagnostic('epub.source.body.start', {uri: source.uri});
20979
+ }
20980
+ const body = await response.arrayBuffer();
20981
+ if (isEpubHttpSource) {
20982
+ sendEpubDiagnostic('epub.source.body.end', {
20983
+ uri: source.uri,
20984
+ byteLength: body.byteLength,
20985
+ });
20986
+ }
20987
+ return body;
20988
+ }
20989
+ if (source.kind === 'base64') {
20990
+ return toExactArrayBuffer(decodeBase64(source.data));
20991
+ }
20992
+ throw new Error('Fonte de quadrinho inválida.');
20993
+ };
20994
+
20995
+ const comicMimeType = (name) => {
20996
+ const extension = name.split('.').pop()?.toLowerCase();
20997
+ if (extension === 'png') return 'image/png';
20998
+ if (extension === 'gif') return 'image/gif';
20999
+ if (extension === 'svg') return 'image/svg+xml';
21000
+ if (extension === 'webp') return 'image/webp';
21001
+ return 'image/jpeg';
21002
+ };
21003
+
21004
+ const updateComicCurrentPage = () => {
21005
+ if (!comicContainer || comicPages.length === 0) return;
21006
+ const viewportTop = comicContainer.scrollTop;
21007
+ let closestIndex = currentPage - 1;
21008
+ let closestDistance = Number.POSITIVE_INFINITY;
21009
+ comicPages.forEach((page, index) => {
21010
+ const distance = Math.abs(page.offsetTop - viewportTop);
21011
+ if (distance < closestDistance) {
21012
+ closestDistance = distance;
21013
+ closestIndex = index;
21014
+ }
21015
+ });
21016
+ const nextPage = Math.max(1, Math.min(pageCount, closestIndex + 1));
21017
+ if (nextPage !== currentPage) {
21018
+ currentPage = nextPage;
21019
+ sendState();
21020
+ }
21021
+ };
21022
+
21023
+ const scheduleComicCurrentPage = () => {
21024
+ if (comicScrollFrame !== null) return;
21025
+ comicScrollFrame = requestAnimationFrame(() => {
21026
+ comicScrollFrame = null;
21027
+ updateComicCurrentPage();
21028
+ });
21029
+ };
21030
+
21031
+ const evictComicUrls = () => {
21032
+ if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) return;
21033
+ const protectedIndexes = comicHelpers.getProtectedComicPageIndexes(
21034
+ currentPage,
21035
+ Array.from(comicVisiblePages),
21036
+ Array.from(comicLoading.keys())
21037
+ );
21038
+ for (const [index, url] of comicObjectUrls) {
21039
+ if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) break;
21040
+ if (protectedIndexes.has(index)) continue;
21041
+ URL.revokeObjectURL(url);
21042
+ comicObjectUrls.delete(index);
21043
+ const image = comicPages[index]?.querySelector('img');
21044
+ const page = comicPages[index];
21045
+ const dimensions = comicDimensions.get(index);
21046
+ const width = dimensions?.width || image?.naturalWidth || 0;
21047
+ const height = dimensions?.height || image?.naturalHeight || 0;
21048
+ if (page && width > 0 && height > 0) {
21049
+ page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
21050
+ width,
21051
+ height
21052
+ );
21053
+ }
21054
+ if (image) image.removeAttribute('src');
21055
+ }
21056
+ };
21057
+
21058
+ const ensureComicPage = async (pageIndex) => {
21059
+ if (comicObjectUrls.has(pageIndex)) {
21060
+ const image = comicPages[pageIndex]?.querySelector('img');
21061
+ if (image && !image.src) image.src = comicObjectUrls.get(pageIndex);
21062
+ return comicObjectUrls.get(pageIndex);
21063
+ }
21064
+ if (comicLoading.has(pageIndex)) return comicLoading.get(pageIndex);
21065
+
21066
+ const entry = comicEntries[pageIndex];
21067
+ if (!entry) throw new Error(`Página de quadrinho inválida: ${pageIndex}`);
21068
+ const loadGeneration = comicGeneration;
21069
+ const pending = entry.async('blob').then((blob) => {
21070
+ if (
21071
+ !comicHelpers.isCurrentComicPageLoad(
21072
+ loadGeneration,
21073
+ comicGeneration,
21074
+ entry,
21075
+ comicEntries[pageIndex]
21076
+ )
21077
+ ) {
21078
+ return null;
21079
+ }
21080
+ const url = URL.createObjectURL(
21081
+ new Blob([blob], { type: comicMimeType(entry.name) })
21082
+ );
21083
+ comicObjectUrls.set(pageIndex, url);
21084
+ const image = comicPages[pageIndex]?.querySelector('img');
21085
+ if (image) image.src = url;
21086
+ evictComicUrls();
21087
+ return url;
21088
+ });
21089
+ comicLoading.set(pageIndex, pending);
21090
+ try {
21091
+ return await pending;
21092
+ } finally {
21093
+ if (comicLoading.get(pageIndex) === pending) {
21094
+ comicLoading.delete(pageIndex);
21095
+ evictComicUrls();
21096
+ }
21097
+ }
21098
+ };
21099
+
21100
+ const renderComicPage = (pageIndex) => {
21101
+ const image = comicPages[pageIndex]?.querySelector('img');
21102
+ if (!image) return;
21103
+ image.style.width = `${Math.max(40, Math.round(100 * zoom))}%`;
21104
+ image.style.transform = `rotate(${rotation}deg)`;
21105
+ };
21106
+
21107
+ const applyComicLayout = () => {
21108
+ comicPages.forEach((_, pageIndex) => renderComicPage(pageIndex));
21109
+ };
21110
+
21111
+ const loadComic = async (source, format) => {
21112
+ clearViewer();
21113
+ let entries;
21114
+ if (format === 'cbr') {
21115
+ const clientUrl = runtimeConfig.cbrClientUrl;
21116
+ const workerUrl = runtimeConfig.cbrWorkerUrl;
21117
+ const wasmUrl = runtimeConfig.cbrWasmUrl;
21118
+ if (!clientUrl || !workerUrl || !wasmUrl) {
21119
+ throw new Error(
21120
+ 'CBR no mobile exige o pacote opcional de runtime libarchive.'
21121
+ );
21122
+ }
20837
21123
 
20838
21124
  const [clientSource, workerSource, wasmSource] = await Promise.all([
20839
21125
  fetchRuntimeAsset(clientUrl, 'text'),
@@ -20845,716 +21131,1003 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20845
21131
  workerSource,
20846
21132
  wasmSource
20847
21133
  );
20848
- const workerObjectUrl = URL.createObjectURL(
20849
- new Blob([patchedWorkerSource], {type: 'text/javascript'})
20850
- );
20851
- const clientObjectUrl = URL.createObjectURL(
20852
- new Blob([clientSource], { type: 'text/javascript' })
20853
- );
20854
- try {
20855
- const archiveModule = await import(clientObjectUrl);
20856
- const createCbrWorker = () => {
20857
- const worker = new Worker(workerObjectUrl, {type: 'module'});
20858
- worker.addEventListener('error', (event) => {
20859
- sendEvent('RUNTIME_ERROR', {
20860
- message: `CBR worker: ${event?.message || 'falha ao executar o worker'}`,
20861
- context: 'comic.worker',
20862
- });
20863
- });
20864
- worker.addEventListener('messageerror', () => {
20865
- sendEvent('RUNTIME_ERROR', {
20866
- message: 'CBR worker: falha ao transferir mensagem',
20867
- context: 'comic.worker',
20868
- });
20869
- });
20870
- return worker;
20871
- };
20872
- archiveModule.Archive.init({getWorker: createCbrWorker});
20873
- const bytes = await sourceToArrayBuffer(source);
20874
- const file = new Blob([bytes], {
20875
- type: 'application/vnd.comicbook-rar',
20876
- });
20877
- const archive = await archiveModule.Archive.open(file);
20878
- const files = await archive.getFilesArray();
20879
- entries = files
20880
- .filter(
20881
- (item) => {
20882
- const name = `${item?.path || ''}${item?.file?.name || ''}`;
20883
- return item?.file && comicHelpers.isComicImageName(name);
20884
- }
20885
- )
20886
- .map((item) => {
20887
- const name = `${item.path || ''}${item.file.name}`;
20888
- return {
20889
- name,
20890
- async: async () => item.file.extract(),
20891
- };
20892
- });
20893
- comicDispose = async () => {
20894
- await archive.close();
20895
- URL.revokeObjectURL(workerObjectUrl);
20896
- URL.revokeObjectURL(clientObjectUrl);
20897
- };
20898
- } catch (error) {
20899
- URL.revokeObjectURL(workerObjectUrl);
20900
- URL.revokeObjectURL(clientObjectUrl);
20901
- throw error;
20902
- }
20903
- } else {
20904
- if (!window.JSZip) {
20905
- throw new Error('O runtime ZIP do leitor mobile não foi carregado.');
21134
+ const workerObjectUrl = URL.createObjectURL(
21135
+ new Blob([patchedWorkerSource], { type: 'text/javascript' })
21136
+ );
21137
+ const clientObjectUrl = URL.createObjectURL(
21138
+ new Blob([clientSource], { type: 'text/javascript' })
21139
+ );
21140
+ try {
21141
+ const archiveModule = await import(clientObjectUrl);
21142
+ const createCbrWorker = () => {
21143
+ const worker = new Worker(workerObjectUrl, {type: 'module'});
21144
+ worker.addEventListener('error', (event) => {
21145
+ sendEvent('RUNTIME_ERROR', {
21146
+ message: `CBR worker: ${event?.message || 'falha ao executar o worker'}`,
21147
+ context: 'comic.worker',
21148
+ });
21149
+ });
21150
+ worker.addEventListener('messageerror', () => {
21151
+ sendEvent('RUNTIME_ERROR', {
21152
+ message: 'CBR worker: falha ao transferir mensagem',
21153
+ context: 'comic.worker',
21154
+ });
21155
+ });
21156
+ return worker;
21157
+ };
21158
+ archiveModule.Archive.init({ getWorker: createCbrWorker });
21159
+ const bytes = await sourceToArrayBuffer(source);
21160
+ const file = new Blob([bytes], {
21161
+ type: 'application/vnd.comicbook-rar',
21162
+ });
21163
+ const archive = await archiveModule.Archive.open(file);
21164
+ const files = await archive.getFilesArray();
21165
+ entries = files
21166
+ .filter(
21167
+ (item) => {
21168
+ const name = `${item?.path || ''}${item?.file?.name || ''}`;
21169
+ return item?.file && comicHelpers.isComicImageName(name);
20906
21170
  }
21171
+ )
21172
+ .map((item) => {
21173
+ const name = `${item.path || ''}${item.file.name}`;
21174
+ return {
21175
+ name,
21176
+ async: async () => item.file.extract(),
21177
+ };
21178
+ });
21179
+ comicDispose = async () => {
21180
+ await archive.close();
21181
+ URL.revokeObjectURL(workerObjectUrl);
21182
+ URL.revokeObjectURL(clientObjectUrl);
21183
+ };
21184
+ } catch (error) {
21185
+ URL.revokeObjectURL(workerObjectUrl);
21186
+ URL.revokeObjectURL(clientObjectUrl);
21187
+ throw error;
21188
+ }
21189
+ } else {
21190
+ if (!window.JSZip) {
21191
+ throw new Error('O runtime ZIP do leitor mobile não foi carregado.');
21192
+ }
20907
21193
 
20908
- const bytes = await sourceToArrayBuffer(source);
20909
- comicArchive = await window.JSZip.loadAsync(bytes);
20910
- entries = Object.keys(comicArchive.files)
20911
- .filter((name) => {
20912
- const entry = comicArchive.files[name];
20913
- return !entry.dir && comicHelpers.isComicImageName(name);
20914
- })
20915
- .map((name) => comicArchive.files[name]);
20916
- }
21194
+ const bytes = await sourceToArrayBuffer(source);
21195
+ comicArchive = await window.JSZip.loadAsync(bytes);
21196
+ entries = Object.keys(comicArchive.files)
21197
+ .filter((name) => {
21198
+ const entry = comicArchive.files[name];
21199
+ return !entry.dir && comicHelpers.isComicImageName(name);
21200
+ })
21201
+ .map((name) => comicArchive.files[name]);
21202
+ }
20917
21203
 
20918
- const names = comicHelpers.sortComicPageNames(
20919
- entries.map((entry) => entry.name)
20920
- );
20921
- const entriesByName = new Map(entries.map((entry) => [entry.name, entry]));
20922
- comicEntries = names.map((name) => entriesByName.get(name));
20923
- if (comicEntries.length === 0) {
20924
- if (comicDispose) {
20925
- await comicDispose();
20926
- comicDispose = null;
20927
- }
20928
- throw new Error('O arquivo não contém páginas de imagem.');
20929
- }
21204
+ const names = comicHelpers.sortComicPageNames(
21205
+ entries.map((entry) => entry.name)
21206
+ );
21207
+ const entriesByName = new Map(entries.map((entry) => [entry.name, entry]));
21208
+ comicEntries = names.map((name) => entriesByName.get(name));
21209
+ if (comicEntries.length === 0) {
21210
+ if (comicDispose) {
21211
+ await comicDispose();
21212
+ comicDispose = null;
21213
+ }
21214
+ throw new Error('O arquivo não contém páginas de imagem.');
21215
+ }
20930
21216
 
20931
- comicContainer = document.createElement('div');
20932
- comicContainer.style.width = '100%';
20933
- comicContainer.style.height = '100%';
20934
- comicContainer.style.overflow = 'auto';
20935
- comicContainer.style.padding = '16px 0 120px';
20936
- comicContainer.style.boxSizing = 'border-box';
20937
- comicContainer.style.background = '#ffffff';
20938
- comicContainer.addEventListener('scroll', scheduleComicCurrentPage, {
20939
- passive: true,
20940
- });
20941
- viewer.style.overflow = 'hidden';
20942
- viewer.appendChild(comicContainer);
21217
+ comicContainer = document.createElement('div');
21218
+ comicContainer.style.width = '100%';
21219
+ comicContainer.style.height = '100%';
21220
+ comicContainer.style.overflow = 'auto';
21221
+ comicContainer.style.padding = '16px 0 120px';
21222
+ comicContainer.style.boxSizing = 'border-box';
21223
+ comicContainer.style.background = '#ffffff';
21224
+ comicContainer.addEventListener('scroll', scheduleComicCurrentPage, {
21225
+ passive: true,
21226
+ });
21227
+ viewer.style.overflow = 'hidden';
21228
+ viewer.appendChild(comicContainer);
20943
21229
 
20944
- comicPages = comicEntries.map((entry, pageIndex) => {
20945
- const page = document.createElement('div');
20946
- page.dataset.pageIndex = String(pageIndex);
20947
- page.style.width = '100%';
20948
- page.style.minHeight = '80px';
20949
- page.style.display = 'flex';
20950
- page.style.justifyContent = 'center';
20951
- page.style.alignItems = 'flex-start';
20952
- page.style.padding = '0 12px 20px';
20953
- page.style.boxSizing = 'border-box';
21230
+ comicPages = comicEntries.map((entry, pageIndex) => {
21231
+ const page = document.createElement('div');
21232
+ page.dataset.pageIndex = String(pageIndex);
21233
+ page.style.width = '100%';
21234
+ page.style.minHeight = '80px';
21235
+ page.style.display = 'flex';
21236
+ page.style.justifyContent = 'center';
21237
+ page.style.alignItems = 'flex-start';
21238
+ page.style.padding = '0 12px 20px';
21239
+ page.style.boxSizing = 'border-box';
20954
21240
 
20955
- const image = document.createElement('img');
20956
- image.alt = `Página ${pageIndex + 1}`;
20957
- image.draggable = false;
20958
- image.decoding = 'async';
20959
- image.loading = 'lazy';
20960
- image.style.display = 'block';
20961
- image.style.maxWidth = 'none';
20962
- image.style.height = 'auto';
20963
- image.style.userSelect = 'none';
20964
- image.addEventListener('load', () => {
20965
- if (image.naturalWidth > 0 && image.naturalHeight > 0) {
20966
- comicDimensions.set(pageIndex, {
20967
- width: image.naturalWidth,
20968
- height: image.naturalHeight,
20969
- });
20970
- page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
20971
- image.naturalWidth,
20972
- image.naturalHeight
20973
- );
20974
- }
20975
- });
20976
- page.appendChild(image);
20977
- comicContainer.appendChild(page);
20978
- return page;
21241
+ const image = document.createElement('img');
21242
+ image.alt = `Página ${pageIndex + 1}`;
21243
+ image.draggable = false;
21244
+ image.decoding = 'async';
21245
+ image.loading = 'lazy';
21246
+ image.style.display = 'block';
21247
+ image.style.maxWidth = 'none';
21248
+ image.style.height = 'auto';
21249
+ image.style.userSelect = 'none';
21250
+ image.addEventListener('load', () => {
21251
+ if (image.naturalWidth > 0 && image.naturalHeight > 0) {
21252
+ comicDimensions.set(pageIndex, {
21253
+ width: image.naturalWidth,
21254
+ height: image.naturalHeight,
20979
21255
  });
21256
+ page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
21257
+ image.naturalWidth,
21258
+ image.naturalHeight
21259
+ );
21260
+ }
21261
+ });
21262
+ page.appendChild(image);
21263
+ comicContainer.appendChild(page);
21264
+ return page;
21265
+ });
20980
21266
 
20981
- if (typeof IntersectionObserver === 'function') {
20982
- comicObserver = new IntersectionObserver(
20983
- (observations) => {
20984
- observations.forEach((observation) => {
20985
- if (!observation.isIntersecting) return;
20986
- const pageIndex = Number(observation.target.dataset.pageIndex);
20987
- void ensureComicPage(pageIndex).catch((error) =>
20988
- sendEvent('RUNTIME_ERROR', {
20989
- message: error?.message || String(error),
20990
- context: 'comic.page',
20991
- })
20992
- );
20993
- });
20994
- scheduleComicCurrentPage();
20995
- },
20996
- { root: comicContainer, rootMargin: '600px 0px' }
20997
- );
20998
- comicPages.forEach((page) => comicObserver.observe(page));
20999
- } else {
21000
- for (let pageIndex = 0; pageIndex < Math.min(3, comicPages.length); pageIndex += 1) {
21001
- void ensureComicPage(pageIndex);
21267
+ if (typeof IntersectionObserver === 'function') {
21268
+ comicObserver = new IntersectionObserver(
21269
+ (observations) => {
21270
+ observations.forEach((observation) => {
21271
+ const pageIndex = Number(observation.target.dataset.pageIndex);
21272
+ if (!observation.isIntersecting) {
21273
+ comicVisiblePages.delete(pageIndex);
21274
+ return;
21002
21275
  }
21003
- }
21004
- pageCount = comicPages.length;
21005
- currentPage = 1;
21006
- await ensureComicPage(0);
21007
- applyComicLayout();
21008
- return { pageCount };
21009
- };
21010
-
21011
- const readComicBlobAsDataUrl = (blob, mimeType) =>
21012
- new Promise((resolve, reject) => {
21013
- const reader = new FileReader();
21014
- reader.onerror = () => reject(new Error('Falha ao gerar preview.'));
21015
- reader.onload = () => resolve(reader.result);
21016
- reader.readAsDataURL(new Blob([blob], { type: mimeType }));
21276
+ comicVisiblePages.add(pageIndex);
21277
+ void ensureComicPage(pageIndex).catch((error) =>
21278
+ sendEvent('RUNTIME_ERROR', {
21279
+ message: error?.message || String(error),
21280
+ context: 'comic.page',
21281
+ })
21282
+ );
21017
21283
  });
21284
+ scheduleComicCurrentPage();
21285
+ },
21286
+ { root: comicContainer, rootMargin: '600px 0px' }
21287
+ );
21288
+ comicPages.forEach((page) => comicObserver.observe(page));
21289
+ } else {
21290
+ for (let pageIndex = 0; pageIndex < Math.min(3, comicPages.length); pageIndex += 1) {
21291
+ void ensureComicPage(pageIndex);
21292
+ }
21293
+ }
21294
+ pageCount = comicPages.length;
21295
+ currentPage = 1;
21296
+ await ensureComicPage(0);
21297
+ applyComicLayout();
21298
+ return { pageCount };
21299
+ };
21018
21300
 
21019
- const createComicPagePreview = async (pageIndex) => {
21020
- const entry = comicEntries[pageIndex];
21021
- if (!entry) return null;
21022
- const mimeType = comicMimeType(entry.name);
21023
- const sourceUrl = await ensureComicPage(pageIndex);
21024
- if (!sourceUrl) return null;
21025
- try {
21026
- const image = new Image();
21027
- image.decoding = 'async';
21028
- await new Promise((resolve, reject) => {
21029
- image.onload = resolve;
21030
- image.onerror = () => reject(new Error('Falha ao decodificar preview.'));
21031
- image.src = sourceUrl;
21032
- });
21033
- const size = comicHelpers.getComicPreviewSize(
21034
- image.naturalWidth || image.width,
21035
- image.naturalHeight || image.height
21036
- );
21037
- const canvas = document.createElement('canvas');
21038
- canvas.width = size.width;
21039
- canvas.height = size.height;
21040
- const context = canvas.getContext('2d');
21041
- if (!context) throw new Error('Canvas indisponível para preview.');
21042
- context.fillStyle = '#ffffff';
21043
- context.fillRect(0, 0, size.width, size.height);
21044
- context.drawImage(image, 0, 0, size.width, size.height);
21045
- return canvas.toDataURL('image/jpeg', 0.78);
21046
- } catch {
21047
- try {
21048
- const response = await fetch(sourceUrl);
21049
- const blob = await response.blob();
21050
- return await readComicBlobAsDataUrl(blob, mimeType);
21051
- } catch {
21052
- return null;
21053
- }
21054
- }
21055
- };
21301
+ const readComicBlobAsDataUrl = (blob, mimeType) =>
21302
+ new Promise((resolve, reject) => {
21303
+ const reader = new FileReader();
21304
+ reader.onerror = () => reject(new Error('Falha ao gerar preview.'));
21305
+ reader.onload = () => resolve(reader.result);
21306
+ reader.readAsDataURL(new Blob([blob], { type: mimeType }));
21307
+ });
21056
21308
 
21057
- const getComicPagePreview = async (pageIndex) => {
21058
- const entry = comicEntries[pageIndex];
21059
- if (!entry) return null;
21060
- if (comicPreviewCache.has(pageIndex)) {
21061
- return comicPreviewCache.get(pageIndex);
21062
- }
21063
- if (comicPreviewLoading.has(pageIndex)) {
21064
- return comicPreviewLoading.get(pageIndex);
21065
- }
21066
- const loadGeneration = comicGeneration;
21067
- const pending = createComicPagePreview(pageIndex).then((preview) => {
21068
- if (
21069
- !comicHelpers.isCurrentComicPageLoad(
21070
- loadGeneration,
21071
- comicGeneration,
21072
- entry,
21073
- comicEntries[pageIndex]
21074
- )
21075
- ) {
21076
- return null;
21077
- }
21078
- comicPreviewCache.set(pageIndex, preview);
21079
- while (comicPreviewCache.size > COMIC_PREVIEW_CACHE_LIMIT) {
21080
- comicPreviewCache.delete(comicPreviewCache.keys().next().value);
21081
- }
21082
- return preview;
21083
- });
21084
- comicPreviewLoading.set(pageIndex, pending);
21085
- try {
21086
- return await pending;
21087
- } finally {
21088
- if (comicPreviewLoading.get(pageIndex) === pending) {
21089
- comicPreviewLoading.delete(pageIndex);
21090
- }
21091
- }
21092
- };
21309
+ const createComicPagePreview = async (pageIndex) => {
21310
+ const entry = comicEntries[pageIndex];
21311
+ if (!entry) return null;
21312
+ const mimeType = comicMimeType(entry.name);
21313
+ const sourceUrl = await ensureComicPage(pageIndex);
21314
+ if (!sourceUrl) return null;
21315
+ try {
21316
+ const image = new Image();
21317
+ image.decoding = 'async';
21318
+ await new Promise((resolve, reject) => {
21319
+ image.onload = resolve;
21320
+ image.onerror = () => reject(new Error('Falha ao decodificar preview.'));
21321
+ image.src = sourceUrl;
21322
+ });
21323
+ const size = comicHelpers.getComicPreviewSize(
21324
+ image.naturalWidth || image.width,
21325
+ image.naturalHeight || image.height
21326
+ );
21327
+ const canvas = document.createElement('canvas');
21328
+ canvas.width = size.width;
21329
+ canvas.height = size.height;
21330
+ const context = canvas.getContext('2d');
21331
+ if (!context) throw new Error('Canvas indisponível para preview.');
21332
+ context.fillStyle = '#ffffff';
21333
+ context.fillRect(0, 0, size.width, size.height);
21334
+ context.drawImage(image, 0, 0, size.width, size.height);
21335
+ return canvas.toDataURL('image/jpeg', 0.78);
21336
+ } catch {
21337
+ try {
21338
+ const response = await fetch(sourceUrl);
21339
+ const blob = await response.blob();
21340
+ return await readComicBlobAsDataUrl(blob, mimeType);
21341
+ } catch {
21342
+ return null;
21343
+ }
21344
+ }
21345
+ };
21093
21346
 
21094
- const getComicPageDimensions = async (pageIndex) => {
21095
- const known = comicDimensions.get(pageIndex);
21096
- if (known) return known;
21097
- await ensureComicPage(pageIndex);
21098
- const image = comicPages[pageIndex]?.querySelector('img');
21099
- if (image && !image.complete) {
21100
- await new Promise((resolve) => {
21101
- image.addEventListener('load', resolve, { once: true });
21102
- image.addEventListener('error', resolve, { once: true });
21103
- });
21104
- }
21105
- return (
21106
- comicDimensions.get(pageIndex) || {
21107
- width: image?.naturalWidth || 0,
21108
- height: image?.naturalHeight || 0,
21109
- }
21110
- );
21111
- };
21347
+ const getComicPagePreview = async (pageIndex) => {
21348
+ const entry = comicEntries[pageIndex];
21349
+ if (!entry) return null;
21350
+ if (comicPreviewCache.has(pageIndex)) {
21351
+ return comicPreviewCache.get(pageIndex);
21352
+ }
21353
+ if (comicPreviewLoading.has(pageIndex)) {
21354
+ return comicPreviewLoading.get(pageIndex);
21355
+ }
21356
+ const loadGeneration = comicGeneration;
21357
+ const pending = createComicPagePreview(pageIndex).then((preview) => {
21358
+ if (
21359
+ !comicHelpers.isCurrentComicPageLoad(
21360
+ loadGeneration,
21361
+ comicGeneration,
21362
+ entry,
21363
+ comicEntries[pageIndex]
21364
+ )
21365
+ ) {
21366
+ return null;
21367
+ }
21368
+ comicPreviewCache.set(pageIndex, preview);
21369
+ while (comicPreviewCache.size > COMIC_PREVIEW_CACHE_LIMIT) {
21370
+ comicPreviewCache.delete(comicPreviewCache.keys().next().value);
21371
+ }
21372
+ return preview;
21373
+ });
21374
+ comicPreviewLoading.set(pageIndex, pending);
21375
+ try {
21376
+ return await pending;
21377
+ } finally {
21378
+ if (comicPreviewLoading.get(pageIndex) === pending) {
21379
+ comicPreviewLoading.delete(pageIndex);
21380
+ }
21381
+ }
21382
+ };
21112
21383
 
21113
- const normalizeHref = (href) => {
21114
- if (!href) return '';
21115
- return href.split('#')[0];
21116
- };
21384
+ const getComicPageDimensions = async (pageIndex) => {
21385
+ const known = comicDimensions.get(pageIndex);
21386
+ if (known) return known;
21387
+ await ensureComicPage(pageIndex);
21388
+ const image = comicPages[pageIndex]?.querySelector('img');
21389
+ if (image && !image.complete) {
21390
+ await new Promise((resolve) => {
21391
+ image.addEventListener('load', resolve, { once: true });
21392
+ image.addEventListener('error', resolve, { once: true });
21393
+ });
21394
+ }
21395
+ return (
21396
+ comicDimensions.get(pageIndex) || {
21397
+ width: image?.naturalWidth || 0,
21398
+ height: image?.naturalHeight || 0,
21399
+ }
21400
+ );
21401
+ };
21117
21402
 
21118
- const getSpineIndexByHref = (href) => {
21119
- const normalized = normalizeHref(href);
21120
- if (!normalized) return -1;
21121
- return spineItems.findIndex((item) => normalizeHref(item.href) === normalized);
21122
- };
21403
+ const normalizeHref = (href) => {
21404
+ if (!href) return '';
21405
+ return href.split('#')[0];
21406
+ };
21123
21407
 
21124
- const buildOutline = async () => {
21125
- if (!book || !book.loaded || !book.loaded.navigation) return [];
21126
- const nav = await book.loaded.navigation;
21127
- const toc = nav && nav.toc ? nav.toc : [];
21408
+ const getSpineIndexByHref = (href) => {
21409
+ const normalized = normalizeHref(href);
21410
+ if (!normalized) return -1;
21411
+ return spineItems.findIndex((item) => normalizeHref(item.href) === normalized);
21412
+ };
21128
21413
 
21129
- const mapItem = (item) => {
21130
- const title = item.label || item.title || '';
21131
- const pageIndex = getSpineIndexByHref(item.href || '');
21132
- const children = Array.isArray(item.subitems) ? item.subitems.map(mapItem) : [];
21133
- const outlineItem = { title, pageIndex };
21134
- if (children.length > 0) outlineItem.children = children;
21135
- return outlineItem;
21136
- };
21414
+ const buildOutline = async () => {
21415
+ if (!book || !book.loaded || !book.loaded.navigation) return [];
21416
+ const nav = await book.loaded.navigation;
21417
+ const toc = nav && nav.toc ? nav.toc : [];
21137
21418
 
21138
- return toc.map(mapItem);
21139
- };
21419
+ const mapItem = (item) => {
21420
+ const title = item.label || item.title || '';
21421
+ const pageIndex = getSpineIndexByHref(item.href || '');
21422
+ const children = Array.isArray(item.subitems) ? item.subitems.map(mapItem) : [];
21423
+ const outlineItem = { title, pageIndex };
21424
+ if (children.length > 0) outlineItem.children = children;
21425
+ return outlineItem;
21426
+ };
21140
21427
 
21141
- const applyEpubZoom = () => {
21142
- if (!rendition || !rendition.themes) return;
21143
- const fontSize = `${Math.round(zoom * 100)}%`;
21144
- if (typeof rendition.themes.fontSize === 'function') {
21145
- rendition.themes.fontSize(fontSize);
21146
- } else if (typeof rendition.themes.override === 'function') {
21147
- rendition.themes.override('font-size', fontSize);
21148
- }
21149
- };
21428
+ return toc.map(mapItem);
21429
+ };
21150
21430
 
21151
- const applyTextZoom = () => {
21152
- if (!textContainer) return;
21153
- const fontSize = Math.max(12, Math.round(DEFAULT_FONT_SIZE * zoom));
21154
- textContainer.style.fontSize = `${fontSize}px`;
21155
- };
21431
+ const applyEpubZoom = () => {
21432
+ if (!rendition || !rendition.themes) return;
21433
+ const fontSize = `${Math.round(zoom * 100)}%`;
21434
+ if (typeof rendition.themes.fontSize === 'function') {
21435
+ rendition.themes.fontSize(fontSize);
21436
+ } else if (typeof rendition.themes.override === 'function') {
21437
+ rendition.themes.override('font-size', fontSize);
21438
+ }
21439
+ };
21156
21440
 
21157
- const renderTextPage = (pageIndex) => {
21158
- const text = textPages[pageIndex] || '';
21159
- if (!textContainer) {
21160
- textContainer = document.createElement('div');
21161
- textContainer.style.padding = '24px';
21162
- textContainer.style.lineHeight = '1.6';
21163
- textContainer.style.whiteSpace = 'pre-wrap';
21164
- textContainer.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
21165
- viewer.appendChild(textContainer);
21166
- }
21167
- textContainer.textContent = text;
21168
- applyTextZoom();
21169
- };
21441
+ const applyTextZoom = () => {
21442
+ if (!textContainer) return;
21443
+ const fontSize = Math.max(12, Math.round(DEFAULT_FONT_SIZE * zoom));
21444
+ textContainer.style.fontSize = `${fontSize}px`;
21445
+ };
21170
21446
 
21171
- const paginateText = (text) => {
21172
- const pages = [];
21173
- for (let i = 0; i < text.length; i += TEXT_PAGE_CHUNK) {
21174
- pages.push(text.slice(i, i + TEXT_PAGE_CHUNK));
21175
- }
21176
- return pages.length > 0 ? pages : [''];
21177
- };
21447
+ const renderTextPage = (pageIndex) => {
21448
+ const text = textPages[pageIndex] || '';
21449
+ if (!textContainer) {
21450
+ textContainer = document.createElement('div');
21451
+ textContainer.style.padding = '24px';
21452
+ textContainer.style.lineHeight = '1.6';
21453
+ textContainer.style.whiteSpace = 'pre-wrap';
21454
+ textContainer.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
21455
+ viewer.appendChild(textContainer);
21456
+ }
21457
+ textContainer.textContent = text;
21458
+ applyTextZoom();
21459
+ };
21178
21460
 
21179
- const loadText = async (source) => {
21180
- let text = '';
21181
- if (source.kind === 'uri') {
21182
- const res = await fetch(source.uri);
21183
- text = await res.text();
21184
- } else if (source.kind === 'base64') {
21185
- text = decodeBase64ToText(source.data);
21186
- } else if (source.kind === 'text') {
21187
- text = source.text || '';
21188
- }
21461
+ const paginateText = (text) => {
21462
+ const pages = [];
21463
+ for (let i = 0; i < text.length; i += TEXT_PAGE_CHUNK) {
21464
+ pages.push(text.slice(i, i + TEXT_PAGE_CHUNK));
21465
+ }
21466
+ return pages.length > 0 ? pages : [''];
21467
+ };
21189
21468
 
21190
- clearViewer();
21191
- textPages = paginateText(text);
21192
- pageCount = textPages.length;
21193
- currentPage = 1;
21194
- textContainer = null;
21195
- renderTextPage(0);
21196
- return { pageCount };
21197
- };
21469
+ const loadText = async (source, isCurrentLoad = () => true) => {
21470
+ let text = '';
21471
+ if (source.kind === 'uri') {
21472
+ const res = await fetch(source.uri);
21473
+ text = await res.text();
21474
+ } else if (source.kind === 'base64') {
21475
+ text = decodeBase64ToText(source.data);
21476
+ } else if (source.kind === 'text') {
21477
+ text = source.text || '';
21478
+ }
21198
21479
 
21199
- const loadEpub = async (source) => {
21200
- if (rendition && typeof rendition.destroy === 'function') {
21201
- rendition.destroy();
21202
- }
21203
- if (book && typeof book.destroy === 'function') {
21204
- book.destroy();
21205
- }
21480
+ if (!isCurrentLoad()) {
21481
+ throw new Error('Document load superseded by a newer load');
21482
+ }
21483
+ clearViewer();
21484
+ textPages = paginateText(text);
21485
+ pageCount = textPages.length;
21486
+ currentPage = 1;
21487
+ textContainer = null;
21488
+ renderTextPage(0);
21489
+ return { pageCount };
21490
+ };
21206
21491
 
21207
- let data = null;
21208
- if (source.kind === 'uri') {
21209
- data = source.uri;
21210
- } else if (source.kind === 'base64') {
21211
- data = decodeBase64(source.data);
21212
- } else if (source.kind === 'text') {
21213
- const encoder = new TextEncoder();
21214
- data = encoder.encode(source.text || '').buffer;
21492
+ const loadEpub = async (source, loadId, generation) => {
21493
+ const diagnosticPayload = {
21494
+ loadId,
21495
+ sourceKind: source.kind,
21496
+ };
21497
+ const isCurrentLoad = () => generation === epubLoadGeneration;
21498
+ sendEpubDiagnostic('epub.load.start', diagnosticPayload);
21499
+
21500
+ if (epubInteractionCleanup) {
21501
+ epubInteractionCleanup();
21502
+ epubInteractionCleanup = null;
21503
+ }
21504
+ if (rendition && typeof rendition.destroy === 'function') {
21505
+ rendition.destroy();
21506
+ }
21507
+ if (book && typeof book.destroy === 'function') {
21508
+ book.destroy();
21509
+ }
21510
+
21511
+ let data = null;
21512
+ if (source.kind === 'uri') {
21513
+ data = await withEpubStageTimeout(
21514
+ sourceToArrayBuffer(source),
21515
+ 'source',
21516
+ diagnosticPayload,
21517
+ );
21518
+ } else if (source.kind === 'base64') {
21519
+ // epub.js expects an ArrayBuffer for binary books. Passing the decoded
21520
+ // Uint8Array directly can leave `book.ready` pending in Android WebView.
21521
+ data = toExactArrayBuffer(decodeBase64(source.data));
21522
+ } else if (source.kind === 'text') {
21523
+ const encoder = new TextEncoder();
21524
+ data = encoder.encode(source.text || '').buffer;
21525
+ }
21526
+
21527
+ if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
21528
+ sendEpubDiagnostic('epub.source.received', {
21529
+ ...diagnosticPayload,
21530
+ byteLength: data?.byteLength,
21531
+ });
21532
+ sendEpubDiagnostic('epub.source.decoded', {
21533
+ ...diagnosticPayload,
21534
+ byteLength: data?.byteLength,
21535
+ });
21536
+ book = ePub(data);
21537
+ sendEpubDiagnostic('epub.book.created', {
21538
+ ...diagnosticPayload,
21539
+ byteLength: data?.byteLength,
21540
+ });
21541
+ sendEpubDiagnostic('epub.book.ready.start', diagnosticPayload);
21542
+ await withEpubStageTimeout(book.ready, 'book.ready', diagnosticPayload);
21543
+ if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
21544
+ sendEpubDiagnostic('epub.book.ready.end', diagnosticPayload);
21545
+
21546
+ spineItems = book.spine && book.spine.items ? book.spine.items : [];
21547
+ pageCount = spineItems.length;
21548
+ currentPage = 1;
21549
+ sendEpubDiagnostic('epub.spine.ready', {
21550
+ ...diagnosticPayload,
21551
+ pageCount,
21552
+ });
21553
+
21554
+ clearViewer();
21555
+ rendition = book.renderTo(viewer, {
21556
+ manager: 'continuous',
21557
+ width: '100%',
21558
+ height: '100%',
21559
+ flow: 'scrolled-continuous',
21560
+ spread: 'none',
21561
+ });
21562
+ sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
21563
+
21564
+ if (rendition && rendition.hooks && rendition.hooks.content) {
21565
+ rendition.hooks.content.register((contents) => {
21566
+ const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
21567
+ if (frame) {
21568
+ frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
21569
+ }
21570
+ const selectionDocument = contents && contents.document;
21571
+ const selectionWindow = contents && contents.window;
21572
+ if (
21573
+ selectionDocument &&
21574
+ typeof selectionDocument.addEventListener === 'function' &&
21575
+ selectionWindow &&
21576
+ typeof selectionWindow.getSelection === 'function'
21577
+ ) {
21578
+ const handleSelectionChange = () => {
21579
+ const selection = selectionWindow.getSelection();
21580
+ const text = selection ? selection.toString().trim() : '';
21581
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
21582
+ };
21583
+ selectionDocument.addEventListener('selectionchange', handleSelectionChange);
21584
+ const selectionCleanup = () => {
21585
+ selectionDocument.removeEventListener('selectionchange', handleSelectionChange);
21586
+ epubSelectionCleanups.delete(selectionCleanup);
21587
+ };
21588
+ epubSelectionCleanups.add(selectionCleanup);
21589
+ if (contents && typeof contents === 'object') {
21590
+ contents.__papyrusSelectionCleanup = selectionCleanup;
21215
21591
  }
21592
+ }
21593
+ });
21594
+ }
21216
21595
 
21217
- book = ePub(data);
21218
- await book.ready;
21596
+ if (
21597
+ rendition &&
21598
+ rendition.hooks &&
21599
+ rendition.hooks.unloaded &&
21600
+ typeof rendition.hooks.unloaded.register === 'function'
21601
+ ) {
21602
+ rendition.hooks.unloaded.register((view) => {
21603
+ const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
21604
+ if (typeof cleanup === 'function') cleanup();
21605
+ });
21606
+ }
21219
21607
 
21220
- spineItems = book.spine && book.spine.items ? book.spine.items : [];
21221
- pageCount = spineItems.length;
21222
- currentPage = 1;
21608
+ if (rendition && typeof rendition.on === 'function') {
21609
+ rendition.on('selected', (cfiRange, contents) => {
21610
+ const selection = contents && contents.window ? contents.window.getSelection() : null;
21611
+ const text = selection ? selection.toString().trim() : '';
21612
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
21613
+ if (text) {
21614
+ sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
21615
+ }
21616
+ if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
21617
+ rendition.annotations.remove(cfiRange, 'highlight');
21618
+ }
21619
+ });
21620
+ rendition.on('relocated', (location) => {
21621
+ const start = location && location.start;
21622
+ let index = start && Number.isInteger(start.index) ? start.index : -1;
21623
+ if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
21624
+ if (index < 0 || index >= spineItems.length) return;
21625
+ epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
21626
+ const nextPage = index + 1;
21627
+ if (nextPage === currentPage) return;
21628
+ currentPage = nextPage;
21629
+ sendState();
21630
+ });
21631
+ }
21223
21632
 
21224
- clearViewer();
21225
- rendition = book.renderTo(viewer, {
21226
- width: '100%',
21227
- height: '100%',
21228
- flow: 'paginated',
21229
- spread: 'none',
21230
- });
21633
+ sendEpubDiagnostic('epub.display.start', diagnosticPayload);
21634
+ await withEpubStageTimeout(
21635
+ displayEpubPage(0),
21636
+ 'rendition.display',
21637
+ diagnosticPayload,
21638
+ );
21639
+ if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
21640
+ sendEpubDiagnostic('epub.display.end', diagnosticPayload);
21641
+ if (
21642
+ rendition &&
21643
+ rendition.manager &&
21644
+ typeof rendition.manager.on === 'function' &&
21645
+ typeof rendition.on === 'function'
21646
+ ) {
21647
+ let touchStart = null;
21648
+ let lastScrollOffset = -1;
21649
+ const manager = rendition.manager;
21650
+ epubScrollDiagnostics = createEpubScrollDiagnostics(manager);
21231
21651
 
21232
- if (rendition && rendition.hooks && rendition.hooks.content) {
21233
- rendition.hooks.content.register((contents) => {
21234
- const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
21235
- if (frame) {
21236
- frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
21237
- }
21652
+ // ContinuousManager schedules checks through its serial queue. Observe
21653
+ // the queue itself so diagnostics can distinguish queued work from the
21654
+ // check currently being executed without changing epub.js semantics.
21655
+ const originalCheck = typeof manager.check === 'function' ? manager.check : null;
21656
+ const queue = manager.q;
21657
+ const originalEnqueue = queue && typeof queue.enqueue === 'function'
21658
+ ? queue.enqueue
21659
+ : null;
21660
+ let checkRequestId = 0;
21661
+ if (originalCheck) {
21662
+ manager.check = function (...args) {
21663
+ const requestId = ++checkRequestId;
21664
+ const checkContext = this;
21665
+ if (epubScrollDiagnostics) {
21666
+ epubScrollDiagnostics.command('manager.check.request', {
21667
+ requestId,
21668
+ queueLength: manager.q && typeof manager.q.length === 'function'
21669
+ ? manager.q.length()
21670
+ : null,
21238
21671
  });
21239
21672
  }
21240
-
21241
- if (rendition && typeof rendition.on === 'function') {
21242
- rendition.on('selected', (cfiRange, contents) => {
21243
- const selection = contents && contents.window ? contents.window.getSelection() : null;
21244
- const text = selection ? selection.toString().trim() : '';
21245
- if (text) {
21246
- sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
21247
- }
21248
- if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
21249
- rendition.annotations.remove(cfiRange, 'highlight');
21250
- }
21673
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.check.start', {
21674
+ requestId,
21675
+ queueLength: queue && typeof queue.length === 'function'
21676
+ ? queue.length()
21677
+ : null,
21678
+ });
21679
+ let result;
21680
+ try {
21681
+ result = originalCheck.apply(checkContext, args);
21682
+ } catch (error) {
21683
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.check.end', {
21684
+ requestId,
21685
+ status: 'error',
21686
+ message: error && error.message ? error.message : String(error),
21251
21687
  });
21688
+ throw error;
21252
21689
  }
21690
+ return Promise.resolve(result)
21691
+ .then(
21692
+ (result) => {
21693
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.check.end', {
21694
+ requestId,
21695
+ status: 'ready',
21696
+ });
21697
+ return result;
21698
+ },
21699
+ (error) => {
21700
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.check.end', {
21701
+ requestId,
21702
+ status: 'error',
21703
+ message: error && error.message ? error.message : String(error),
21704
+ });
21705
+ throw error;
21706
+ },
21707
+ );
21708
+ };
21709
+ }
21710
+ if (originalEnqueue) {
21711
+ queue.enqueue = function (...args) {
21712
+ const queueLengthBefore = typeof queue.length === 'function' ? queue.length() : null;
21713
+ const result = originalEnqueue.apply(this, args);
21714
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.queue.enqueue', {
21715
+ queueLengthBefore,
21716
+ queueLengthAfter: typeof queue.length === 'function' ? queue.length() : null,
21717
+ running: Boolean(queue.running),
21718
+ });
21719
+ return result;
21720
+ };
21721
+ }
21722
+ const getTouchPoint = (event) => {
21723
+ const touch =
21724
+ (event && event.changedTouches && event.changedTouches[0]) ||
21725
+ (event && event.touches && event.touches[0]);
21726
+ if (!touch) return null;
21727
+ const x = Number(touch.screenX ?? touch.clientX);
21728
+ const y = Number(touch.screenY ?? touch.clientY);
21729
+ return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
21730
+ };
21731
+ const handleScroll = (event) => {
21732
+ const offsetY = Math.max(0, Number(event && event.top) || 0);
21733
+ epubScrollDiagnostics && epubScrollDiagnostics.scroll(event);
21734
+ if (Math.abs(offsetY - lastScrollOffset) < 1) return;
21735
+ lastScrollOffset = offsetY;
21736
+ sendEvent('VIEWER_SCROLL', { offsetY });
21737
+ };
21738
+ const handleTouchStart = (event) => {
21739
+ touchStart = getTouchPoint(event);
21740
+ epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
21741
+ };
21742
+ const handleTouchMove = (event) => {
21743
+ epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
21744
+ };
21745
+ const handleTouchEnd = (event) => {
21746
+ const end = getTouchPoint(event);
21747
+ const start = touchStart;
21748
+ touchStart = null;
21749
+ if (start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
21750
+ sendEvent('VIEWER_TAP', {});
21751
+ }
21752
+ epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
21753
+ };
21253
21754
 
21254
- await displayEpubPage(0);
21255
- applyEpubZoom();
21755
+ manager.on('scroll', handleScroll);
21756
+ const handleScrolled = (event) => {
21757
+ epubScrollDiagnostics && epubScrollDiagnostics.settled(event);
21758
+ };
21759
+ manager.on('scrolled', handleScrolled);
21760
+ rendition.on('touchstart', handleTouchStart);
21761
+ rendition.on('touchmove', handleTouchMove);
21762
+ rendition.on('touchend', handleTouchEnd);
21763
+ epubInteractionCleanup = () => {
21764
+ epubSelectionCleanups.forEach((cleanup) => cleanup());
21765
+ epubSelectionCleanups.clear();
21766
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(false);
21767
+ if (originalCheck) manager.check = originalCheck;
21768
+ if (originalEnqueue) queue.enqueue = originalEnqueue;
21769
+ if (typeof manager.off === 'function') manager.off('scroll', handleScroll);
21770
+ if (typeof manager.off === 'function') manager.off('scrolled', handleScrolled);
21771
+ if (typeof rendition.off === 'function') {
21772
+ rendition.off('touchstart', handleTouchStart);
21773
+ rendition.off('touchmove', handleTouchMove);
21774
+ rendition.off('touchend', handleTouchEnd);
21775
+ }
21776
+ epubScrollDiagnostics = null;
21777
+ };
21778
+ }
21779
+ applyEpubZoom();
21256
21780
 
21257
- const outline = await buildOutline();
21258
- return { pageCount, outline };
21259
- };
21781
+ const outline = await withEpubStageTimeout(
21782
+ buildOutline(),
21783
+ 'outline',
21784
+ diagnosticPayload,
21785
+ );
21786
+ if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
21787
+ sendEpubDiagnostic('epub.load.ready', {
21788
+ ...diagnosticPayload,
21789
+ pageCount,
21790
+ });
21791
+ return { pageCount, outline };
21792
+ };
21260
21793
 
21261
- const displayEpubPage = async (pageIndex) => {
21262
- if (!rendition) return;
21263
- const item = spineItems[pageIndex];
21264
- if (!item) return;
21265
- const target = item.href || item.idref || item.cfiBase || pageIndex;
21266
- await rendition.display(target);
21267
- currentPage = pageIndex + 1;
21268
- sendState();
21269
- };
21794
+ const displayEpubPage = async (pageIndex) => {
21795
+ if (!rendition) return;
21796
+ const item = spineItems[pageIndex];
21797
+ if (!item) return;
21798
+ const target = item.href || item.idref || item.cfiBase || pageIndex;
21799
+ epubScrollDiagnostics && epubScrollDiagnostics.command('display', {
21800
+ target,
21801
+ pageIndex,
21802
+ });
21803
+ await rendition.display(target);
21804
+ currentPage = pageIndex + 1;
21805
+ sendState();
21806
+ };
21270
21807
 
21271
- const getTextContent = async (pageIndex) => {
21272
- if (currentType === 'text') {
21273
- const text = textPages[pageIndex] || '';
21274
- return [{
21275
- str: text,
21276
- dir: 'ltr',
21277
- width: 0,
21278
- height: 0,
21279
- transform: [1, 0, 0, 1, 0, 0],
21280
- fontName: 'default',
21281
- }];
21282
- }
21808
+ const getTextContent = async (pageIndex) => {
21809
+ if (currentType === 'text') {
21810
+ const text = textPages[pageIndex] || '';
21811
+ return [{
21812
+ str: text,
21813
+ dir: 'ltr',
21814
+ width: 0,
21815
+ height: 0,
21816
+ transform: [1, 0, 0, 1, 0, 0],
21817
+ fontName: 'default',
21818
+ }];
21819
+ }
21283
21820
 
21284
- if (currentType === 'epub') {
21285
- if (!book) return [];
21286
- const item = spineItems[pageIndex];
21287
- if (!item) return [];
21288
- try {
21289
- const section = book.spine.get(item.idref || item.href);
21290
- const text = section && typeof section.text === 'function' ? await section.text() : '';
21291
- if (!text) return [];
21292
- return [{
21293
- str: text,
21294
- dir: 'ltr',
21295
- width: 0,
21296
- height: 0,
21297
- transform: [1, 0, 0, 1, 0, 0],
21298
- fontName: 'default',
21299
- }];
21300
- } catch (err) {
21301
- return [];
21302
- }
21303
- }
21821
+ if (currentType === 'epub') {
21822
+ if (!book) return [];
21823
+ const item = spineItems[pageIndex];
21824
+ if (!item) return [];
21825
+ try {
21826
+ const section = book.spine.get(item.idref || item.href);
21827
+ const text = section && typeof section.text === 'function' ? await section.text() : '';
21828
+ if (!text) return [];
21829
+ return [{
21830
+ str: text,
21831
+ dir: 'ltr',
21832
+ width: 0,
21833
+ height: 0,
21834
+ transform: [1, 0, 0, 1, 0, 0],
21835
+ fontName: 'default',
21836
+ }];
21837
+ } catch (err) {
21838
+ return [];
21839
+ }
21840
+ }
21304
21841
 
21305
- return [];
21306
- };
21842
+ return [];
21843
+ };
21307
21844
 
21308
- const getPageText = async (pageIndex) => {
21309
- const items = await getTextContent(pageIndex);
21310
- return items.map((item) => item.str).join(' ');
21311
- };
21845
+ const getPageText = async (pageIndex) => {
21846
+ const items = await getTextContent(pageIndex);
21847
+ return items.map((item) => item.str).join(' ');
21848
+ };
21312
21849
 
21313
- const searchText = async (query) => {
21314
- const normalized = query.toLowerCase();
21315
- const results = [];
21850
+ const searchText = async (query) => {
21851
+ const normalized = query.toLowerCase();
21852
+ const results = [];
21316
21853
 
21317
- for (let i = 0; i < pageCount; i += 1) {
21318
- const text = await getPageText(i);
21319
- if (!text) continue;
21320
- const lower = text.toLowerCase();
21321
- let pos = lower.indexOf(normalized, 0);
21322
- let matchIndex = 0;
21323
- while (pos !== -1) {
21324
- const start = Math.max(0, pos - 20);
21325
- const end = Math.min(text.length, pos + query.length + 20);
21326
- results.push({ pageIndex: i, text: text.substring(start, end), matchIndex });
21327
- matchIndex += 1;
21328
- pos = lower.indexOf(normalized, pos + 1);
21329
- }
21330
- }
21854
+ for (let i = 0; i < pageCount; i += 1) {
21855
+ const text = await getPageText(i);
21856
+ if (!text) continue;
21857
+ const lower = text.toLowerCase();
21858
+ let pos = lower.indexOf(normalized, 0);
21859
+ let matchIndex = 0;
21860
+ while (pos !== -1) {
21861
+ const start = Math.max(0, pos - 20);
21862
+ const end = Math.min(text.length, pos + query.length + 20);
21863
+ results.push({ pageIndex: i, text: text.substring(start, end), matchIndex });
21864
+ matchIndex += 1;
21865
+ pos = lower.indexOf(normalized, pos + 1);
21866
+ }
21867
+ }
21331
21868
 
21332
- return results;
21333
- };
21869
+ return results;
21870
+ };
21334
21871
 
21335
- const getPageDimensions = () => ({
21336
- width: viewer.clientWidth || 0,
21337
- height: viewer.clientHeight || 0,
21338
- });
21872
+ const getPageDimensions = () => ({
21873
+ width: viewer.clientWidth || 0,
21874
+ height: viewer.clientHeight || 0,
21875
+ });
21339
21876
 
21340
- const getPageIndex = (dest) => {
21341
- if (!dest) return null;
21342
- if (typeof dest === 'string') return getSpineIndexByHref(dest);
21343
- if (typeof dest !== 'object') return null;
21344
- if (dest.kind === 'href') return getSpineIndexByHref(dest.value);
21345
- if (dest.kind === 'pageIndex') return dest.value;
21346
- if (dest.kind === 'pageNumber') return Math.max(0, dest.value - 1);
21347
- return null;
21348
- };
21877
+ const getPageIndex = (dest) => {
21878
+ if (!dest) return null;
21879
+ if (typeof dest === 'string') return getSpineIndexByHref(dest);
21880
+ if (typeof dest !== 'object') return null;
21881
+ if (dest.kind === 'href') return getSpineIndexByHref(dest.value);
21882
+ if (dest.kind === 'pageIndex') return dest.value;
21883
+ if (dest.kind === 'pageNumber') return Math.max(0, dest.value - 1);
21884
+ return null;
21885
+ };
21349
21886
 
21350
- const selectText = async (pageIndex) => {
21351
- const text = await getPageText(pageIndex);
21352
- if (!text) return null;
21353
- return {
21354
- text,
21355
- rects: [{ x: 0, y: 0, width: 1, height: 1 }],
21356
- };
21357
- };
21887
+ const selectText = async (pageIndex) => {
21888
+ const text = await getPageText(pageIndex);
21889
+ if (!text) return null;
21890
+ return {
21891
+ text,
21892
+ rects: [{ x: 0, y: 0, width: 1, height: 1 }],
21893
+ };
21894
+ };
21358
21895
 
21359
- const applyZoom = () => {
21360
- if (currentType === 'text') {
21361
- applyTextZoom();
21362
- } else if (currentType === 'epub') {
21363
- applyEpubZoom();
21364
- } else if (currentType === 'comic') {
21365
- applyComicLayout();
21366
- }
21367
- };
21896
+ const applyZoom = () => {
21897
+ if (currentType === 'text') {
21898
+ applyTextZoom();
21899
+ } else if (currentType === 'epub') {
21900
+ applyEpubZoom();
21901
+ } else if (currentType === 'comic') {
21902
+ applyComicLayout();
21903
+ }
21904
+ };
21368
21905
 
21369
- const handleCommand = async (message) => {
21370
- const { id, kind, payload } = message;
21906
+ const handleCommand = async (message) => {
21907
+ const { id, kind, payload } = message;
21371
21908
 
21372
- try {
21373
- if (kind === 'load') {
21374
- currentType = payload.type;
21375
- zoom = 1.0;
21909
+ try {
21910
+ if (kind === 'load') {
21911
+ const generation = (epubLoadGeneration += 1);
21912
+ currentType = payload.type;
21913
+ zoom = 1.0;
21376
21914
 
21377
- if (currentType === 'text') {
21378
- const result = await loadText(payload.source);
21379
- sendState();
21380
- sendResponse(id, true, result);
21381
- return;
21382
- }
21915
+ if (currentType === 'text') {
21916
+ const result = await loadText(
21917
+ payload.source,
21918
+ () => generation === epubLoadGeneration,
21919
+ );
21920
+ if (generation !== epubLoadGeneration) {
21921
+ throw new Error('Document load superseded by a newer load');
21922
+ }
21923
+ sendEvent('document.ready', {
21924
+ loadId: id,
21925
+ pageCount: result.pageCount,
21926
+ });
21927
+ sendState();
21928
+ sendResponse(id, true, result);
21929
+ return;
21930
+ }
21383
21931
 
21384
- if (currentType === 'epub') {
21385
- const result = await loadEpub(payload.source);
21386
- sendState({ outline: result.outline || [] });
21387
- sendResponse(id, true, result);
21388
- return;
21389
- }
21932
+ if (currentType === 'epub') {
21933
+ const result = await loadEpub(payload.source, id, generation);
21934
+ sendEvent('document.ready', {
21935
+ loadId: id,
21936
+ pageCount: result.pageCount,
21937
+ });
21938
+ sendState({ outline: result.outline || [] });
21939
+ sendResponse(id, true, result);
21940
+ return;
21941
+ }
21390
21942
 
21391
- if (currentType === 'comic') {
21392
- const result = await loadComic(payload.source, payload.format || 'cbz');
21393
- sendState();
21394
- sendResponse(id, true, result);
21395
- return;
21396
- }
21943
+ if (currentType === 'comic') {
21944
+ const result = await loadComic(payload.source, payload.format || 'cbz');
21945
+ sendState();
21946
+ sendResponse(id, true, result);
21947
+ return;
21948
+ }
21397
21949
 
21398
- throw new Error('Unsupported document type');
21399
- }
21950
+ throw new Error('Unsupported document type');
21951
+ }
21400
21952
 
21401
- if (kind === 'go-to-page') {
21402
- const page = Math.max(1, payload.page || 1);
21403
- if (currentType === 'text') {
21404
- currentPage = page;
21405
- renderTextPage(page - 1);
21406
- sendState();
21407
- } else if (currentType === 'epub') {
21408
- await displayEpubPage(page - 1);
21409
- } else if (currentType === 'comic') {
21410
- currentPage = page;
21411
- const target = comicPages[page - 1];
21412
- if (target) {
21413
- await ensureComicPage(page - 1);
21414
- target.scrollIntoView({ block: 'start' });
21415
- renderComicPage(page - 1);
21416
- sendState();
21417
- }
21418
- }
21419
- sendResponse(id, true, { currentPage });
21420
- return;
21421
- }
21953
+ if (kind === 'go-to-page') {
21954
+ const page = Math.max(1, payload.page || 1);
21955
+ if (currentType === 'text') {
21956
+ currentPage = page;
21957
+ renderTextPage(page - 1);
21958
+ sendState();
21959
+ } else if (currentType === 'epub') {
21960
+ await displayEpubPage(page - 1);
21961
+ } else if (currentType === 'comic') {
21962
+ currentPage = page;
21963
+ const target = comicPages[page - 1];
21964
+ if (target) {
21965
+ await ensureComicPage(page - 1);
21966
+ target.scrollIntoView({ block: 'start' });
21967
+ renderComicPage(page - 1);
21968
+ sendState();
21969
+ }
21970
+ }
21971
+ sendResponse(id, true, { currentPage });
21972
+ return;
21973
+ }
21422
21974
 
21423
- if (kind === 'set-zoom') {
21424
- zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
21425
- applyZoom();
21426
- sendState();
21427
- sendResponse(id, true, { zoom });
21428
- return;
21429
- }
21975
+ if (kind === 'set-zoom') {
21976
+ zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
21977
+ applyZoom();
21978
+ sendState();
21979
+ sendResponse(id, true, { zoom });
21980
+ return;
21981
+ }
21430
21982
 
21431
- if (kind === 'set-rotation') {
21432
- rotation = ((payload.rotation || 0) % 360 + 360) % 360;
21433
- if (currentType === 'comic') applyComicLayout();
21434
- sendResponse(id, true, {});
21435
- return;
21436
- }
21983
+ if (kind === 'set-rotation') {
21984
+ rotation = ((payload.rotation || 0) % 360 + 360) % 360;
21985
+ if (currentType === 'comic') applyComicLayout();
21986
+ sendResponse(id, true, {});
21987
+ return;
21988
+ }
21437
21989
 
21438
- if (kind === 'get-text-content') {
21439
- const items = await getTextContent(payload.pageIndex || 0);
21440
- sendResponse(id, true, items);
21441
- return;
21442
- }
21990
+ if (kind === 'get-text-content') {
21991
+ const items = await getTextContent(payload.pageIndex || 0);
21992
+ sendResponse(id, true, items);
21993
+ return;
21994
+ }
21443
21995
 
21444
- if (kind === 'get-page-dimensions') {
21445
- if (currentType === 'comic') {
21446
- sendResponse(
21447
- id,
21448
- true,
21449
- await getComicPageDimensions(payload.pageIndex || 0)
21450
- );
21451
- } else {
21452
- sendResponse(id, true, getPageDimensions());
21453
- }
21454
- return;
21455
- }
21996
+ if (kind === 'get-page-dimensions') {
21997
+ if (currentType === 'comic') {
21998
+ sendResponse(
21999
+ id,
22000
+ true,
22001
+ await getComicPageDimensions(payload.pageIndex || 0)
22002
+ );
22003
+ } else {
22004
+ sendResponse(id, true, getPageDimensions());
22005
+ }
22006
+ return;
22007
+ }
21456
22008
 
21457
- if (kind === 'get-page-preview') {
21458
- if (currentType !== 'comic') {
21459
- sendResponse(id, true, null);
21460
- return;
21461
- }
21462
- sendResponse(
21463
- id,
21464
- true,
21465
- await getComicPagePreview(payload.pageIndex || 0)
21466
- );
21467
- return;
21468
- }
22009
+ if (kind === 'get-page-preview') {
22010
+ if (currentType !== 'comic') {
22011
+ sendResponse(id, true, null);
22012
+ return;
22013
+ }
22014
+ sendResponse(
22015
+ id,
22016
+ true,
22017
+ await getComicPagePreview(payload.pageIndex || 0)
22018
+ );
22019
+ return;
22020
+ }
21469
22021
 
21470
- if (kind === 'search-text') {
21471
- const results = await searchText(payload.query || '');
21472
- sendResponse(id, true, results);
21473
- return;
21474
- }
22022
+ if (kind === 'search-text') {
22023
+ const results = await searchText(payload.query || '');
22024
+ sendResponse(id, true, results);
22025
+ return;
22026
+ }
21475
22027
 
21476
- if (kind === 'select-text') {
21477
- const selection = await selectText(payload.pageIndex || 0);
21478
- sendResponse(id, true, selection);
21479
- return;
21480
- }
22028
+ if (kind === 'select-text') {
22029
+ const selection = await selectText(payload.pageIndex || 0);
22030
+ sendResponse(id, true, selection);
22031
+ return;
22032
+ }
21481
22033
 
21482
- if (kind === 'get-outline') {
21483
- const outline = await buildOutline();
21484
- sendResponse(id, true, outline);
21485
- return;
21486
- }
22034
+ if (kind === 'get-outline') {
22035
+ const outline = await buildOutline();
22036
+ sendResponse(id, true, outline);
22037
+ return;
22038
+ }
21487
22039
 
21488
- if (kind === 'get-page-index') {
21489
- const index = getPageIndex(payload.dest);
21490
- sendResponse(id, true, index);
21491
- return;
21492
- }
22040
+ if (kind === 'get-page-index') {
22041
+ const index = getPageIndex(payload.dest);
22042
+ sendResponse(id, true, index);
22043
+ return;
22044
+ }
21493
22045
 
21494
- if (kind === 'destroy') {
21495
- clearViewer();
21496
- sendResponse(id, true, {});
21497
- return;
21498
- }
22046
+ if (kind === 'destroy') {
22047
+ clearViewer();
22048
+ sendResponse(id, true, {});
22049
+ return;
22050
+ }
21499
22051
 
21500
- sendResponse(id, false, null, 'Unknown command');
21501
- } catch (err) {
21502
- sendResponse(id, false, null, err && err.message ? err.message : String(err));
21503
- }
21504
- };
22052
+ sendResponse(id, false, null, 'Unknown command');
22053
+ } catch (err) {
22054
+ const errorMessage = err && err.message ? err.message : String(err);
22055
+ const isSuperseded = /superseded by a newer load/i.test(errorMessage);
22056
+ if (kind === 'load' && isSuperseded) {
22057
+ sendEvent('document.stale', { loadId: id, error: errorMessage });
22058
+ } else if (kind === 'load') {
22059
+ sendEvent('document.error', {
22060
+ loadId: id,
22061
+ error: { message: errorMessage, stack: err?.stack || null },
22062
+ });
22063
+ }
22064
+ if (kind === 'load' && payload?.type === 'epub') {
22065
+ sendEpubDiagnostic('epub.load.error', {
22066
+ loadId: id,
22067
+ sourceKind: payload.source?.kind,
22068
+ error: { message: errorMessage, stack: err?.stack || null },
22069
+ });
22070
+ sendEvent('document.error', {
22071
+ loadId: id,
22072
+ error: { message: errorMessage, stack: err?.stack || null },
22073
+ });
22074
+ }
22075
+ sendResponse(id, false, null, errorMessage);
22076
+ }
22077
+ };
21505
22078
 
21506
- const onMessage = (event) => {
21507
- let message = null;
21508
- const raw = event && typeof event === 'object' ? event.data : null;
22079
+ const onMessage = (event) => {
22080
+ let message = null;
22081
+ const raw = event && typeof event === 'object' ? event.data : null;
21509
22082
 
21510
- if (raw && typeof raw === 'object' && (raw.kind || raw.type) && raw.id) {
21511
- message = raw;
21512
- } else if (typeof raw === 'string') {
21513
- if (raw.startsWith('setImmediate$')) return;
21514
- const trimmed = raw.trim();
21515
- if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
21516
- return;
21517
- }
21518
- try {
21519
- message = JSON.parse(trimmed);
21520
- } catch (err) {
21521
- return;
21522
- }
21523
- } else {
21524
- return;
21525
- }
22083
+ if (raw && typeof raw === 'object' && (raw.kind || raw.type) && raw.id) {
22084
+ message = raw;
22085
+ } else if (typeof raw === 'string') {
22086
+ if (raw.startsWith('setImmediate$')) return;
22087
+ const trimmed = raw.trim();
22088
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
22089
+ return;
22090
+ }
22091
+ try {
22092
+ message = JSON.parse(trimmed);
22093
+ } catch (err) {
22094
+ return;
22095
+ }
22096
+ } else {
22097
+ return;
22098
+ }
21526
22099
 
21527
- if (!message || message.id == null) return;
22100
+ if (!message || message.id == null) return;
21528
22101
 
21529
- if (message.type === 'asset-response') {
21530
- const pending = runtimeAssetRequests.get(message.id);
21531
- if (!pending) return;
21532
- runtimeAssetRequests.delete(message.id);
21533
- clearTimeout(pending.timeout);
21534
- if (message.ok) pending.resolve(message.data);
21535
- else pending.reject(new Error(message.error || 'Falha ao carregar asset CBR.'));
21536
- return;
21537
- }
22102
+ if (message.type === 'asset-response') {
22103
+ const pending = runtimeAssetRequests.get(message.id);
22104
+ if (!pending) return;
22105
+ runtimeAssetRequests.delete(message.id);
22106
+ clearTimeout(pending.timeout);
22107
+ if (message.ok) pending.resolve(message.data);
22108
+ else pending.reject(new Error(message.error || 'Falha ao carregar asset CBR.'));
22109
+ return;
22110
+ }
21538
22111
 
21539
- if (message.type === 'file-chunk-response') {
21540
- const pending = fileChunkRequests.get(message.id);
21541
- if (!pending) return;
21542
- fileChunkRequests.delete(message.id);
21543
- clearTimeout(pending.timeout);
21544
- if (message.ok) pending.resolve(message);
21545
- else pending.reject(new Error(message.error || 'Falha ao ler arquivo local.'));
21546
- return;
21547
- }
22112
+ if (message.type === 'file-chunk-response') {
22113
+ const pending = fileChunkRequests.get(message.id);
22114
+ if (!pending) return;
22115
+ fileChunkRequests.delete(message.id);
22116
+ clearTimeout(pending.timeout);
22117
+ if (message.ok) pending.resolve(message);
22118
+ else pending.reject(new Error(message.error || 'Falha ao ler arquivo local.'));
22119
+ return;
22120
+ }
21548
22121
 
21549
- if (!message.kind) return;
21550
- handleCommand(message);
21551
- };
22122
+ if (!message.kind) return;
22123
+ handleCommand(message);
22124
+ };
21552
22125
 
21553
- window.addEventListener('message', onMessage);
21554
- document.addEventListener('message', onMessage);
22126
+ window.addEventListener('message', onMessage);
22127
+ document.addEventListener('message', onMessage);
21555
22128
 
21556
- sendMessage({ type: 'ready' });
21557
- })();
22129
+ sendMessage({ type: 'ready' });
22130
+ })();
21558
22131
  </script>
21559
22132
  </body>
21560
22133
  </html>