@papyrus-sdk/ui-react-native 0.2.19 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20403,9 +20403,121 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20403
20403
  ]).default;
20404
20404
  });
20405
20405
  </script>
20406
+ <!-- @papyrus-comic-runtime:start -->
20407
+ <script>
20408
+ (function (root, factory) {
20409
+ if (typeof module === "object" && module.exports) {
20410
+ module.exports = factory();
20411
+ } else {
20412
+ root.PapyrusComicRuntime = Object.assign(
20413
+ factory(),
20414
+ root.PapyrusComicRuntime || {}
20415
+ );
20416
+ }
20417
+ })(typeof self === "object" ? self : this, function () {
20418
+ var IMAGE_EXTENSIONS = /\\.(?:gif|jpe?g|png|svg|webp)$/i;
20419
+
20420
+ function isComicImageName(name) {
20421
+ return typeof name === "string" && IMAGE_EXTENSIONS.test(name);
20422
+ }
20423
+
20424
+ function naturalCompare(left, right) {
20425
+ var leftParts = String(left).split(/(\\d+)/);
20426
+ var rightParts = String(right).split(/(\\d+)/);
20427
+ var length = Math.max(leftParts.length, rightParts.length);
20428
+
20429
+ for (var index = 0; index < length; index += 1) {
20430
+ var leftPart = leftParts[index] || "";
20431
+ var rightPart = rightParts[index] || "";
20432
+ if (leftPart === rightPart) continue;
20433
+
20434
+ var leftNumber = /^\\d+$/.test(leftPart) ? Number(leftPart) : null;
20435
+ var rightNumber = /^\\d+$/.test(rightPart) ? Number(rightPart) : null;
20436
+ if (leftNumber !== null && rightNumber !== null) {
20437
+ return leftNumber - rightNumber;
20438
+ }
20439
+ return leftPart.localeCompare(rightPart, undefined, {
20440
+ numeric: true,
20441
+ sensitivity: "base",
20442
+ });
20443
+ }
20444
+
20445
+ return 0;
20446
+ }
20447
+
20448
+ function sortComicPageNames(names) {
20449
+ return names.slice().sort(naturalCompare);
20450
+ }
20451
+
20452
+ function patchCbrWorkerSource(workerSource, wasmUrl) {
20453
+ return workerSource.replace(
20454
+ /new URL\\("libarchive\\.wasm",import\\.meta\\.url\\)\\.href/g,
20455
+ JSON.stringify(wasmUrl)
20456
+ );
20457
+ }
20458
+
20459
+ function getComicPreviewSize(width, height, maxWidth, maxHeight) {
20460
+ var safeWidth = Math.max(1, Number(width) || 1);
20461
+ var safeHeight = Math.max(1, Number(height) || 1);
20462
+ var widthLimit = maxWidth || 240;
20463
+ var heightLimit = maxHeight || 360;
20464
+ var scale = Math.min(1, widthLimit / safeWidth, heightLimit / safeHeight);
20465
+ return {
20466
+ width: Math.max(1, Math.round(safeWidth * scale)),
20467
+ height: Math.max(1, Math.round(safeHeight * scale)),
20468
+ };
20469
+ }
20470
+
20471
+ function getComicPageAspectRatio(width, height) {
20472
+ var safeWidth = Math.max(1, Number(width) || 1);
20473
+ var safeHeight = Math.max(1, Number(height) || 1);
20474
+ return safeWidth + " / " + safeHeight;
20475
+ }
20476
+
20477
+ function getProtectedComicPageIndexes(
20478
+ currentPage,
20479
+ visiblePageIndexes,
20480
+ loadingPageIndexes
20481
+ ) {
20482
+ var protectedIndexes = new Set([
20483
+ currentPage - 1,
20484
+ currentPage,
20485
+ currentPage - 2,
20486
+ ]);
20487
+ (visiblePageIndexes || []).forEach(function (pageIndex) {
20488
+ protectedIndexes.add(pageIndex);
20489
+ });
20490
+ (loadingPageIndexes || []).forEach(function (pageIndex) {
20491
+ protectedIndexes.add(pageIndex);
20492
+ });
20493
+ return protectedIndexes;
20494
+ }
20495
+
20496
+ function isCurrentComicPageLoad(
20497
+ loadGeneration,
20498
+ currentGeneration,
20499
+ entry,
20500
+ currentEntry
20501
+ ) {
20502
+ return loadGeneration === currentGeneration && entry === currentEntry;
20503
+ }
20504
+
20505
+ return {
20506
+ isComicImageName: isComicImageName,
20507
+ sortComicPageNames: sortComicPageNames,
20508
+ patchCbrWorkerSource: patchCbrWorkerSource,
20509
+ getComicPreviewSize: getComicPreviewSize,
20510
+ getComicPageAspectRatio: getComicPageAspectRatio,
20511
+ getProtectedComicPageIndexes: getProtectedComicPageIndexes,
20512
+ isCurrentComicPageLoad: isCurrentComicPageLoad,
20513
+ };
20514
+ });
20515
+
20516
+ </script>
20517
+ <!-- @papyrus-comic-runtime:end -->
20406
20518
  <script>
20407
20519
  (function () {
20408
- const viewer = document.getElementById("viewer");
20520
+ const viewer = document.getElementById('viewer');
20409
20521
  const DEFAULT_FONT_SIZE = 16;
20410
20522
  const TEXT_PAGE_CHUNK = 1600;
20411
20523
 
@@ -20418,27 +20530,116 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20418
20530
  let currentPage = 1;
20419
20531
  let pageCount = 0;
20420
20532
  let zoom = 1.0;
20533
+ let rotation = 0;
20534
+ let comicArchive = null;
20535
+ let comicEntries = [];
20536
+ let comicContainer = null;
20537
+ let comicPages = [];
20538
+ let comicObserver = null;
20539
+ let comicScrollFrame = null;
20540
+ const comicObjectUrls = new Map();
20541
+ const comicLoading = new Map();
20542
+ const comicDimensions = new Map();
20543
+ const comicPreviewCache = new Map();
20544
+ const comicPreviewLoading = new Map();
20545
+ const COMIC_CACHE_LIMIT = 16;
20546
+ const COMIC_PREVIEW_CACHE_LIMIT = 8;
20547
+ const comicHelpers = window.PapyrusComicRuntime || {};
20548
+ const runtimeConfig = window.__PAPYRUS_RUNTIME_CONFIG__ || {};
20549
+ let comicDispose = null;
20550
+ let comicGeneration = 0;
20421
20551
 
20422
20552
  const sendMessage = (payload) => {
20423
- if (
20424
- window.ReactNativeWebView &&
20425
- window.ReactNativeWebView.postMessage
20426
- ) {
20553
+ if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
20427
20554
  window.ReactNativeWebView.postMessage(JSON.stringify(payload));
20428
20555
  return;
20429
20556
  }
20430
20557
  if (window.parent && window.parent !== window) {
20431
- window.parent.postMessage(payload, "*");
20558
+ window.parent.postMessage(payload, '*');
20432
20559
  }
20433
20560
  };
20434
20561
 
20435
20562
  const sendResponse = (id, ok, data, error) => {
20436
- sendMessage({ type: "response", id, ok, data, error });
20563
+ sendMessage({ type: 'response', id, ok, data, error });
20564
+ };
20565
+
20566
+ const runtimeAssetRequests = new Map();
20567
+ const fileChunkRequests = new Map();
20568
+
20569
+ const requestRuntimeAsset = (url, encoding) =>
20570
+ new Promise((resolve, reject) => {
20571
+ const id = \`runtime-asset-\${Date.now()}-\${Math.random()}\`;
20572
+ const timeout = setTimeout(() => {
20573
+ runtimeAssetRequests.delete(id);
20574
+ reject(new Error(\`Tempo esgotado ao carregar asset CBR: \${url}\`));
20575
+ }, 15000);
20576
+ runtimeAssetRequests.set(id, {resolve, reject, timeout});
20577
+ sendMessage({type: 'asset-request', id, url, encoding});
20578
+ });
20579
+
20580
+ const encodeArrayBufferAsDataUrl = async (buffer) => {
20581
+ const bytes = new Uint8Array(buffer);
20582
+ let binary = '';
20583
+ const chunkSize = 0x8000;
20584
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
20585
+ binary += String.fromCharCode(
20586
+ ...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),
20587
+ );
20588
+ }
20589
+ return \`data:application/wasm;base64,\${btoa(binary)}\`;
20590
+ };
20591
+
20592
+ const fetchRuntimeAsset = async (url, encoding) => {
20593
+ try {
20594
+ const response = await fetch(url);
20595
+ if (!response.ok) throw new Error(\`status \${response.status}\`);
20596
+ if (encoding === 'text') return await response.text();
20597
+ return await encodeArrayBufferAsDataUrl(await response.arrayBuffer());
20598
+ } catch {
20599
+ return await requestRuntimeAsset(url, encoding);
20600
+ }
20601
+ };
20602
+
20603
+ const requestLocalFileChunk = (uri, offset, length) =>
20604
+ new Promise((resolve, reject) => {
20605
+ const id = \`file-chunk-\${Date.now()}-\${Math.random()}\`;
20606
+ const timeout = setTimeout(() => {
20607
+ fileChunkRequests.delete(id);
20608
+ reject(new Error(\`Tempo esgotado ao ler arquivo local: \${uri}\`));
20609
+ }, 30000);
20610
+ fileChunkRequests.set(id, {resolve, reject, timeout});
20611
+ sendMessage({type: 'file-chunk-request', id, uri, offset, length});
20612
+ });
20613
+
20614
+ const readLocalFile = async (uri) => {
20615
+ const chunkSize = 1024 * 1024;
20616
+ const chunks = [];
20617
+ let offset = 0;
20618
+ let totalLength = 0;
20619
+ while (true) {
20620
+ const result = await requestLocalFileChunk(uri, offset, chunkSize);
20621
+ const bytes = decodeBase64(result.data || '');
20622
+ if (bytes.length === 0 && !result.done) {
20623
+ throw new Error('Leitura nativa retornou um chunk vazio.');
20624
+ }
20625
+ chunks.push(bytes);
20626
+ totalLength += bytes.length;
20627
+ offset += bytes.length;
20628
+ if (result.done) break;
20629
+ }
20630
+
20631
+ const output = new Uint8Array(totalLength);
20632
+ let outputOffset = 0;
20633
+ chunks.forEach((chunk) => {
20634
+ output.set(chunk, outputOffset);
20635
+ outputOffset += chunk.length;
20636
+ });
20637
+ return output.buffer;
20437
20638
  };
20438
20639
 
20439
20640
  const sendState = (extra) => {
20440
20641
  sendMessage({
20441
- type: "state",
20642
+ type: 'state',
20442
20643
  payload: {
20443
20644
  pageCount,
20444
20645
  currentPage,
@@ -20449,70 +20650,66 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20449
20650
  };
20450
20651
 
20451
20652
  const sendEvent = (name, payload) => {
20452
- sendMessage({ type: "event", name, payload });
20453
- };
20454
-
20455
- const log = (...args) => {
20456
- sendEvent("RUNTIME_LOG", {
20457
- message: args.map((value) => String(value)).join(" "),
20458
- });
20653
+ sendMessage({ type: 'event', name, payload });
20459
20654
  };
20460
20655
 
20461
20656
  const reportError = (error, context) => {
20462
- const message =
20463
- error && error.message ? error.message : String(error);
20657
+ const message = error && error.message ? error.message : String(error);
20464
20658
  const stack = error && error.stack ? error.stack : null;
20465
- sendEvent("RUNTIME_ERROR", { message, context, stack });
20466
- };
20467
-
20468
- const EPUB_OPEN_TIMEOUT_MS = 180000;
20469
- const EPUB_READY_TIMEOUT_MS = 180000;
20470
- const EPUB_DISPLAY_TIMEOUT_MS = 30000;
20471
-
20472
- const withTimeout = async (promise, ms, label) => {
20473
- let timer = null;
20474
- try {
20475
- return await Promise.race([
20476
- promise,
20477
- new Promise((_, reject) => {
20478
- timer = setTimeout(
20479
- () => reject(new Error(\`Timeout: \${label}\`)),
20480
- ms
20481
- );
20482
- }),
20483
- ]);
20484
- } finally {
20485
- if (timer) {
20486
- clearTimeout(timer);
20487
- }
20488
- }
20659
+ sendEvent('RUNTIME_ERROR', { message, context, stack });
20489
20660
  };
20490
20661
 
20491
20662
  const shouldIgnoreError = (error) => {
20492
- const message =
20493
- error && error.message ? error.message : String(error || "");
20494
- return message.includes("ResizeObserver loop");
20663
+ const message = error && error.message ? error.message : String(error || '');
20664
+ return message.includes('ResizeObserver loop');
20495
20665
  };
20496
20666
 
20497
- window.addEventListener("error", (event) => {
20667
+ window.addEventListener('error', (event) => {
20498
20668
  const error = event.error || event.message;
20499
20669
  if (shouldIgnoreError(error)) return;
20500
- reportError(error, "window.error");
20670
+ reportError(error, 'window.error');
20501
20671
  });
20502
20672
 
20503
- window.addEventListener("unhandledrejection", (event) => {
20673
+ window.addEventListener('unhandledrejection', (event) => {
20504
20674
  if (shouldIgnoreError(event.reason)) return;
20505
- reportError(event.reason, "unhandledrejection");
20675
+ reportError(event.reason, 'unhandledrejection');
20506
20676
  });
20507
20677
 
20678
+ const clearComicState = () => {
20679
+ comicGeneration += 1;
20680
+ if (comicObserver) {
20681
+ comicObserver.disconnect();
20682
+ comicObserver = null;
20683
+ }
20684
+ if (comicScrollFrame !== null) {
20685
+ cancelAnimationFrame(comicScrollFrame);
20686
+ comicScrollFrame = null;
20687
+ }
20688
+ comicObjectUrls.forEach((url) => URL.revokeObjectURL(url));
20689
+ comicObjectUrls.clear();
20690
+ comicLoading.clear();
20691
+ comicDimensions.clear();
20692
+ comicPreviewCache.clear();
20693
+ comicPreviewLoading.clear();
20694
+ if (comicDispose) {
20695
+ void comicDispose();
20696
+ comicDispose = null;
20697
+ }
20698
+ comicArchive = null;
20699
+ comicEntries = [];
20700
+ comicContainer = null;
20701
+ comicPages = [];
20702
+ };
20703
+
20508
20704
  const clearViewer = () => {
20705
+ clearComicState();
20509
20706
  while (viewer.firstChild) {
20510
20707
  viewer.removeChild(viewer.firstChild);
20511
20708
  }
20512
20709
  };
20513
20710
 
20514
20711
  const decodeBase64 = (value) => {
20515
- const clean = value.replace(/\\s/g, "");
20712
+ const clean = value.replace(/\\s/g, '');
20516
20713
  const binary = atob(clean);
20517
20714
  const len = binary.length;
20518
20715
  const bytes = new Uint8Array(len);
@@ -20524,41 +20721,438 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20524
20721
 
20525
20722
  const decodeBase64ToText = (value) => {
20526
20723
  const bytes = decodeBase64(value);
20527
- return new TextDecoder("utf-8").decode(bytes);
20724
+ return new TextDecoder('utf-8').decode(bytes);
20528
20725
  };
20529
20726
 
20530
- const fetchUriAsArrayBuffer = async (uri) => {
20531
- const response = await fetch(uri);
20532
- const ok = response.ok || response.status === 0;
20533
- if (!ok) {
20534
- throw new Error(
20535
- "Failed to fetch epub uri (" + response.status + ")"
20727
+ const sourceToArrayBuffer = async (source) => {
20728
+ if (source.kind === 'uri') {
20729
+ if (/^(?:file|content):\\/\\//i.test(source.uri)) {
20730
+ return readLocalFile(source.uri);
20731
+ }
20732
+ const response = await fetch(source.uri);
20733
+ if (!response.ok && response.status !== 0) {
20734
+ throw new Error(\`Falha ao carregar quadrinho (\${response.status})\`);
20735
+ }
20736
+ return response.arrayBuffer();
20737
+ }
20738
+ if (source.kind === 'base64') {
20739
+ return decodeBase64(source.data).buffer;
20740
+ }
20741
+ throw new Error('Fonte de quadrinho inv\xE1lida.');
20742
+ };
20743
+
20744
+ const comicMimeType = (name) => {
20745
+ const extension = name.split('.').pop()?.toLowerCase();
20746
+ if (extension === 'png') return 'image/png';
20747
+ if (extension === 'gif') return 'image/gif';
20748
+ if (extension === 'svg') return 'image/svg+xml';
20749
+ if (extension === 'webp') return 'image/webp';
20750
+ return 'image/jpeg';
20751
+ };
20752
+
20753
+ const updateComicCurrentPage = () => {
20754
+ if (!comicContainer || comicPages.length === 0) return;
20755
+ const viewportTop = comicContainer.scrollTop;
20756
+ let closestIndex = currentPage - 1;
20757
+ let closestDistance = Number.POSITIVE_INFINITY;
20758
+ comicPages.forEach((page, index) => {
20759
+ const distance = Math.abs(page.offsetTop - viewportTop);
20760
+ if (distance < closestDistance) {
20761
+ closestDistance = distance;
20762
+ closestIndex = index;
20763
+ }
20764
+ });
20765
+ const nextPage = Math.max(1, Math.min(pageCount, closestIndex + 1));
20766
+ if (nextPage !== currentPage) {
20767
+ currentPage = nextPage;
20768
+ sendState();
20769
+ }
20770
+ };
20771
+
20772
+ const scheduleComicCurrentPage = () => {
20773
+ if (comicScrollFrame !== null) return;
20774
+ comicScrollFrame = requestAnimationFrame(() => {
20775
+ comicScrollFrame = null;
20776
+ updateComicCurrentPage();
20777
+ });
20778
+ };
20779
+
20780
+ const evictComicUrls = () => {
20781
+ if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) return;
20782
+ const protectedIndexes = new Set([
20783
+ currentPage - 1,
20784
+ currentPage,
20785
+ currentPage - 2,
20786
+ ]);
20787
+ for (const [index, url] of comicObjectUrls) {
20788
+ if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) break;
20789
+ if (protectedIndexes.has(index)) continue;
20790
+ URL.revokeObjectURL(url);
20791
+ comicObjectUrls.delete(index);
20792
+ const image = comicPages[index]?.querySelector('img');
20793
+ const page = comicPages[index];
20794
+ const dimensions = comicDimensions.get(index);
20795
+ const width = dimensions?.width || image?.naturalWidth || 0;
20796
+ const height = dimensions?.height || image?.naturalHeight || 0;
20797
+ if (page && width > 0 && height > 0) {
20798
+ page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
20799
+ width,
20800
+ height
20801
+ );
20802
+ }
20803
+ if (image) image.removeAttribute('src');
20804
+ }
20805
+ };
20806
+
20807
+ const ensureComicPage = async (pageIndex) => {
20808
+ if (comicObjectUrls.has(pageIndex)) {
20809
+ const image = comicPages[pageIndex]?.querySelector('img');
20810
+ if (image && !image.src) image.src = comicObjectUrls.get(pageIndex);
20811
+ return comicObjectUrls.get(pageIndex);
20812
+ }
20813
+ if (comicLoading.has(pageIndex)) return comicLoading.get(pageIndex);
20814
+
20815
+ const entry = comicEntries[pageIndex];
20816
+ if (!entry) throw new Error(\`P\xE1gina de quadrinho inv\xE1lida: \${pageIndex}\`);
20817
+ const loadGeneration = comicGeneration;
20818
+ const pending = entry.async('blob').then((blob) => {
20819
+ if (
20820
+ !comicHelpers.isCurrentComicPageLoad(
20821
+ loadGeneration,
20822
+ comicGeneration,
20823
+ entry,
20824
+ comicEntries[pageIndex]
20825
+ )
20826
+ ) {
20827
+ return null;
20828
+ }
20829
+ const url = URL.createObjectURL(
20830
+ new Blob([blob], { type: comicMimeType(entry.name) })
20536
20831
  );
20832
+ comicObjectUrls.set(pageIndex, url);
20833
+ const image = comicPages[pageIndex]?.querySelector('img');
20834
+ if (image) image.src = url;
20835
+ evictComicUrls();
20836
+ return url;
20837
+ });
20838
+ comicLoading.set(pageIndex, pending);
20839
+ try {
20840
+ return await pending;
20841
+ } finally {
20842
+ if (comicLoading.get(pageIndex) === pending) {
20843
+ comicLoading.delete(pageIndex);
20844
+ }
20537
20845
  }
20538
- if (typeof response.arrayBuffer === "function") {
20539
- return response.arrayBuffer();
20846
+ };
20847
+
20848
+ const renderComicPage = (pageIndex) => {
20849
+ const image = comicPages[pageIndex]?.querySelector('img');
20850
+ if (!image) return;
20851
+ image.style.width = \`\${Math.max(40, Math.round(100 * zoom))}%\`;
20852
+ image.style.transform = \`rotate(\${rotation}deg)\`;
20853
+ };
20854
+
20855
+ const applyComicLayout = () => {
20856
+ comicPages.forEach((_, pageIndex) => renderComicPage(pageIndex));
20857
+ };
20858
+
20859
+ const loadComic = async (source, format) => {
20860
+ clearViewer();
20861
+ let entries;
20862
+ if (format === 'cbr') {
20863
+ const clientUrl = runtimeConfig.cbrClientUrl;
20864
+ const workerUrl = runtimeConfig.cbrWorkerUrl;
20865
+ const wasmUrl = runtimeConfig.cbrWasmUrl;
20866
+ if (!clientUrl || !workerUrl || !wasmUrl) {
20867
+ throw new Error(
20868
+ 'CBR no mobile exige o pacote opcional de runtime libarchive.'
20869
+ );
20870
+ }
20871
+
20872
+ const [clientSource, workerSource, wasmSource] = await Promise.all([
20873
+ fetchRuntimeAsset(clientUrl, 'text'),
20874
+ fetchRuntimeAsset(workerUrl, 'text'),
20875
+ runtimeConfig.cbrWasmDataUrl ||
20876
+ fetchRuntimeAsset(wasmUrl, 'base64'),
20877
+ ]);
20878
+ const patchedWorkerSource = comicHelpers.patchCbrWorkerSource(
20879
+ workerSource,
20880
+ wasmSource
20881
+ );
20882
+ const workerObjectUrl = URL.createObjectURL(
20883
+ new Blob([patchedWorkerSource], {type: 'text/javascript'})
20884
+ );
20885
+ const clientObjectUrl = URL.createObjectURL(
20886
+ new Blob([clientSource], { type: 'text/javascript' })
20887
+ );
20888
+ try {
20889
+ const archiveModule = await import(clientObjectUrl);
20890
+ const createCbrWorker = () => {
20891
+ const worker = new Worker(workerObjectUrl, {type: 'module'});
20892
+ worker.addEventListener('error', (event) => {
20893
+ sendEvent('RUNTIME_ERROR', {
20894
+ message: \`CBR worker: \${event?.message || 'falha ao executar o worker'}\`,
20895
+ context: 'comic.worker',
20896
+ });
20897
+ });
20898
+ worker.addEventListener('messageerror', () => {
20899
+ sendEvent('RUNTIME_ERROR', {
20900
+ message: 'CBR worker: falha ao transferir mensagem',
20901
+ context: 'comic.worker',
20902
+ });
20903
+ });
20904
+ return worker;
20905
+ };
20906
+ archiveModule.Archive.init({getWorker: createCbrWorker});
20907
+ const bytes = await sourceToArrayBuffer(source);
20908
+ const file = new Blob([bytes], {
20909
+ type: 'application/vnd.comicbook-rar',
20910
+ });
20911
+ const archive = await archiveModule.Archive.open(file);
20912
+ const files = await archive.getFilesArray();
20913
+ entries = files
20914
+ .filter(
20915
+ (item) => {
20916
+ const name = \`\${item?.path || ''}\${item?.file?.name || ''}\`;
20917
+ return item?.file && comicHelpers.isComicImageName(name);
20918
+ }
20919
+ )
20920
+ .map((item) => {
20921
+ const name = \`\${item.path || ''}\${item.file.name}\`;
20922
+ return {
20923
+ name,
20924
+ async: async () => item.file.extract(),
20925
+ };
20926
+ });
20927
+ comicDispose = async () => {
20928
+ await archive.close();
20929
+ URL.revokeObjectURL(workerObjectUrl);
20930
+ URL.revokeObjectURL(clientObjectUrl);
20931
+ };
20932
+ } catch (error) {
20933
+ URL.revokeObjectURL(workerObjectUrl);
20934
+ URL.revokeObjectURL(clientObjectUrl);
20935
+ throw error;
20936
+ }
20937
+ } else {
20938
+ if (!window.JSZip) {
20939
+ throw new Error('O runtime ZIP do leitor mobile n\xE3o foi carregado.');
20940
+ }
20941
+
20942
+ const bytes = await sourceToArrayBuffer(source);
20943
+ comicArchive = await window.JSZip.loadAsync(bytes);
20944
+ entries = Object.keys(comicArchive.files)
20945
+ .filter((name) => {
20946
+ const entry = comicArchive.files[name];
20947
+ return !entry.dir && comicHelpers.isComicImageName(name);
20948
+ })
20949
+ .map((name) => comicArchive.files[name]);
20950
+ }
20951
+
20952
+ const names = comicHelpers.sortComicPageNames(
20953
+ entries.map((entry) => entry.name)
20954
+ );
20955
+ const entriesByName = new Map(entries.map((entry) => [entry.name, entry]));
20956
+ comicEntries = names.map((name) => entriesByName.get(name));
20957
+ if (comicEntries.length === 0) {
20958
+ if (comicDispose) {
20959
+ await comicDispose();
20960
+ comicDispose = null;
20961
+ }
20962
+ throw new Error('O arquivo n\xE3o cont\xE9m p\xE1ginas de imagem.');
20540
20963
  }
20541
- const blob = await response.blob();
20542
- return new Promise((resolve, reject) => {
20964
+
20965
+ comicContainer = document.createElement('div');
20966
+ comicContainer.style.width = '100%';
20967
+ comicContainer.style.height = '100%';
20968
+ comicContainer.style.overflow = 'auto';
20969
+ comicContainer.style.padding = '16px 0 120px';
20970
+ comicContainer.style.boxSizing = 'border-box';
20971
+ comicContainer.style.background = '#ffffff';
20972
+ comicContainer.addEventListener('scroll', scheduleComicCurrentPage, {
20973
+ passive: true,
20974
+ });
20975
+ viewer.style.overflow = 'hidden';
20976
+ viewer.appendChild(comicContainer);
20977
+
20978
+ comicPages = comicEntries.map((entry, pageIndex) => {
20979
+ const page = document.createElement('div');
20980
+ page.dataset.pageIndex = String(pageIndex);
20981
+ page.style.width = '100%';
20982
+ page.style.minHeight = '80px';
20983
+ page.style.display = 'flex';
20984
+ page.style.justifyContent = 'center';
20985
+ page.style.alignItems = 'flex-start';
20986
+ page.style.padding = '0 12px 20px';
20987
+ page.style.boxSizing = 'border-box';
20988
+
20989
+ const image = document.createElement('img');
20990
+ image.alt = \`P\xE1gina \${pageIndex + 1}\`;
20991
+ image.draggable = false;
20992
+ image.decoding = 'async';
20993
+ image.loading = 'lazy';
20994
+ image.style.display = 'block';
20995
+ image.style.maxWidth = 'none';
20996
+ image.style.height = 'auto';
20997
+ image.style.userSelect = 'none';
20998
+ image.addEventListener('load', () => {
20999
+ if (image.naturalWidth > 0 && image.naturalHeight > 0) {
21000
+ comicDimensions.set(pageIndex, {
21001
+ width: image.naturalWidth,
21002
+ height: image.naturalHeight,
21003
+ });
21004
+ page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
21005
+ image.naturalWidth,
21006
+ image.naturalHeight
21007
+ );
21008
+ }
21009
+ });
21010
+ page.appendChild(image);
21011
+ comicContainer.appendChild(page);
21012
+ return page;
21013
+ });
21014
+
21015
+ if (typeof IntersectionObserver === 'function') {
21016
+ comicObserver = new IntersectionObserver(
21017
+ (observations) => {
21018
+ observations.forEach((observation) => {
21019
+ if (!observation.isIntersecting) return;
21020
+ const pageIndex = Number(observation.target.dataset.pageIndex);
21021
+ void ensureComicPage(pageIndex).catch((error) =>
21022
+ sendEvent('RUNTIME_ERROR', {
21023
+ message: error?.message || String(error),
21024
+ context: 'comic.page',
21025
+ })
21026
+ );
21027
+ });
21028
+ scheduleComicCurrentPage();
21029
+ },
21030
+ { root: comicContainer, rootMargin: '600px 0px' }
21031
+ );
21032
+ comicPages.forEach((page) => comicObserver.observe(page));
21033
+ } else {
21034
+ for (let pageIndex = 0; pageIndex < Math.min(3, comicPages.length); pageIndex += 1) {
21035
+ void ensureComicPage(pageIndex);
21036
+ }
21037
+ }
21038
+ pageCount = comicPages.length;
21039
+ currentPage = 1;
21040
+ await ensureComicPage(0);
21041
+ applyComicLayout();
21042
+ return { pageCount };
21043
+ };
21044
+
21045
+ const readComicBlobAsDataUrl = (blob, mimeType) =>
21046
+ new Promise((resolve, reject) => {
20543
21047
  const reader = new FileReader();
20544
- reader.onerror = () =>
20545
- reject(new Error("Failed to read epub blob"));
21048
+ reader.onerror = () => reject(new Error('Falha ao gerar preview.'));
20546
21049
  reader.onload = () => resolve(reader.result);
20547
- reader.readAsArrayBuffer(blob);
21050
+ reader.readAsDataURL(new Blob([blob], { type: mimeType }));
21051
+ });
21052
+
21053
+ const createComicPagePreview = async (pageIndex) => {
21054
+ const entry = comicEntries[pageIndex];
21055
+ if (!entry) return null;
21056
+ const mimeType = comicMimeType(entry.name);
21057
+ const sourceUrl = await ensureComicPage(pageIndex);
21058
+ if (!sourceUrl) return null;
21059
+ try {
21060
+ const image = new Image();
21061
+ image.decoding = 'async';
21062
+ await new Promise((resolve, reject) => {
21063
+ image.onload = resolve;
21064
+ image.onerror = () => reject(new Error('Falha ao decodificar preview.'));
21065
+ image.src = sourceUrl;
21066
+ });
21067
+ const size = comicHelpers.getComicPreviewSize(
21068
+ image.naturalWidth || image.width,
21069
+ image.naturalHeight || image.height
21070
+ );
21071
+ const canvas = document.createElement('canvas');
21072
+ canvas.width = size.width;
21073
+ canvas.height = size.height;
21074
+ const context = canvas.getContext('2d');
21075
+ if (!context) throw new Error('Canvas indispon\xEDvel para preview.');
21076
+ context.fillStyle = '#ffffff';
21077
+ context.fillRect(0, 0, size.width, size.height);
21078
+ context.drawImage(image, 0, 0, size.width, size.height);
21079
+ return canvas.toDataURL('image/jpeg', 0.78);
21080
+ } catch {
21081
+ try {
21082
+ const response = await fetch(sourceUrl);
21083
+ const blob = await response.blob();
21084
+ return await readComicBlobAsDataUrl(blob, mimeType);
21085
+ } catch {
21086
+ return null;
21087
+ }
21088
+ }
21089
+ };
21090
+
21091
+ const getComicPagePreview = async (pageIndex) => {
21092
+ const entry = comicEntries[pageIndex];
21093
+ if (!entry) return null;
21094
+ if (comicPreviewCache.has(pageIndex)) {
21095
+ return comicPreviewCache.get(pageIndex);
21096
+ }
21097
+ if (comicPreviewLoading.has(pageIndex)) {
21098
+ return comicPreviewLoading.get(pageIndex);
21099
+ }
21100
+ const loadGeneration = comicGeneration;
21101
+ const pending = createComicPagePreview(pageIndex).then((preview) => {
21102
+ if (
21103
+ !comicHelpers.isCurrentComicPageLoad(
21104
+ loadGeneration,
21105
+ comicGeneration,
21106
+ entry,
21107
+ comicEntries[pageIndex]
21108
+ )
21109
+ ) {
21110
+ return null;
21111
+ }
21112
+ comicPreviewCache.set(pageIndex, preview);
21113
+ while (comicPreviewCache.size > COMIC_PREVIEW_CACHE_LIMIT) {
21114
+ comicPreviewCache.delete(comicPreviewCache.keys().next().value);
21115
+ }
21116
+ return preview;
20548
21117
  });
21118
+ comicPreviewLoading.set(pageIndex, pending);
21119
+ try {
21120
+ return await pending;
21121
+ } finally {
21122
+ if (comicPreviewLoading.get(pageIndex) === pending) {
21123
+ comicPreviewLoading.delete(pageIndex);
21124
+ }
21125
+ }
21126
+ };
21127
+
21128
+ const getComicPageDimensions = async (pageIndex) => {
21129
+ const known = comicDimensions.get(pageIndex);
21130
+ if (known) return known;
21131
+ await ensureComicPage(pageIndex);
21132
+ const image = comicPages[pageIndex]?.querySelector('img');
21133
+ if (image && !image.complete) {
21134
+ await new Promise((resolve) => {
21135
+ image.addEventListener('load', resolve, { once: true });
21136
+ image.addEventListener('error', resolve, { once: true });
21137
+ });
21138
+ }
21139
+ return (
21140
+ comicDimensions.get(pageIndex) || {
21141
+ width: image?.naturalWidth || 0,
21142
+ height: image?.naturalHeight || 0,
21143
+ }
21144
+ );
20549
21145
  };
20550
21146
 
20551
21147
  const normalizeHref = (href) => {
20552
- if (!href) return "";
20553
- return href.split("#")[0];
21148
+ if (!href) return '';
21149
+ return href.split('#')[0];
20554
21150
  };
20555
21151
 
20556
21152
  const getSpineIndexByHref = (href) => {
20557
21153
  const normalized = normalizeHref(href);
20558
21154
  if (!normalized) return -1;
20559
- return spineItems.findIndex(
20560
- (item) => normalizeHref(item.href) === normalized
20561
- );
21155
+ return spineItems.findIndex((item) => normalizeHref(item.href) === normalized);
20562
21156
  };
20563
21157
 
20564
21158
  const buildOutline = async () => {
@@ -20567,11 +21161,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20567
21161
  const toc = nav && nav.toc ? nav.toc : [];
20568
21162
 
20569
21163
  const mapItem = (item) => {
20570
- const title = item.label || item.title || "";
20571
- const pageIndex = getSpineIndexByHref(item.href || "");
20572
- const children = Array.isArray(item.subitems)
20573
- ? item.subitems.map(mapItem)
20574
- : [];
21164
+ const title = item.label || item.title || '';
21165
+ const pageIndex = getSpineIndexByHref(item.href || '');
21166
+ const children = Array.isArray(item.subitems) ? item.subitems.map(mapItem) : [];
20575
21167
  const outlineItem = { title, pageIndex };
20576
21168
  if (children.length > 0) outlineItem.children = children;
20577
21169
  return outlineItem;
@@ -20583,10 +21175,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20583
21175
  const applyEpubZoom = () => {
20584
21176
  if (!rendition || !rendition.themes) return;
20585
21177
  const fontSize = \`\${Math.round(zoom * 100)}%\`;
20586
- if (typeof rendition.themes.fontSize === "function") {
21178
+ if (typeof rendition.themes.fontSize === 'function') {
20587
21179
  rendition.themes.fontSize(fontSize);
20588
- } else if (typeof rendition.themes.override === "function") {
20589
- rendition.themes.override("font-size", fontSize);
21180
+ } else if (typeof rendition.themes.override === 'function') {
21181
+ rendition.themes.override('font-size', fontSize);
20590
21182
  }
20591
21183
  };
20592
21184
 
@@ -20597,14 +21189,13 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20597
21189
  };
20598
21190
 
20599
21191
  const renderTextPage = (pageIndex) => {
20600
- const text = textPages[pageIndex] || "";
21192
+ const text = textPages[pageIndex] || '';
20601
21193
  if (!textContainer) {
20602
- textContainer = document.createElement("div");
20603
- textContainer.style.padding = "24px";
20604
- textContainer.style.lineHeight = "1.6";
20605
- textContainer.style.whiteSpace = "pre-wrap";
20606
- textContainer.style.fontFamily =
20607
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
21194
+ textContainer = document.createElement('div');
21195
+ textContainer.style.padding = '24px';
21196
+ textContainer.style.lineHeight = '1.6';
21197
+ textContainer.style.whiteSpace = 'pre-wrap';
21198
+ textContainer.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
20608
21199
  viewer.appendChild(textContainer);
20609
21200
  }
20610
21201
  textContainer.textContent = text;
@@ -20616,18 +21207,18 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20616
21207
  for (let i = 0; i < text.length; i += TEXT_PAGE_CHUNK) {
20617
21208
  pages.push(text.slice(i, i + TEXT_PAGE_CHUNK));
20618
21209
  }
20619
- return pages.length > 0 ? pages : [""];
21210
+ return pages.length > 0 ? pages : [''];
20620
21211
  };
20621
21212
 
20622
21213
  const loadText = async (source) => {
20623
- let text = "";
20624
- if (source.kind === "uri") {
21214
+ let text = '';
21215
+ if (source.kind === 'uri') {
20625
21216
  const res = await fetch(source.uri);
20626
21217
  text = await res.text();
20627
- } else if (source.kind === "base64") {
21218
+ } else if (source.kind === 'base64') {
20628
21219
  text = decodeBase64ToText(source.data);
20629
- } else if (source.kind === "text") {
20630
- text = source.text || "";
21220
+ } else if (source.kind === 'text') {
21221
+ text = source.text || '';
20631
21222
  }
20632
21223
 
20633
21224
  clearViewer();
@@ -20640,44 +21231,25 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20640
21231
  };
20641
21232
 
20642
21233
  const loadEpub = async (source) => {
20643
- log("epub.load:start", source.kind);
20644
- if (rendition && typeof rendition.destroy === "function") {
21234
+ if (rendition && typeof rendition.destroy === 'function') {
20645
21235
  rendition.destroy();
20646
21236
  }
20647
- if (book && typeof book.destroy === "function") {
21237
+ if (book && typeof book.destroy === 'function') {
20648
21238
  book.destroy();
20649
21239
  }
20650
21240
 
20651
- if (source.kind === "base64") {
20652
- book = ePub();
20653
- await withTimeout(
20654
- book.open(source.data, "base64"),
20655
- EPUB_OPEN_TIMEOUT_MS,
20656
- "epub.open"
20657
- );
20658
- } else if (source.kind === "uri") {
20659
- log("epub.load:fetch-uri");
20660
- const data = await withTimeout(
20661
- fetchUriAsArrayBuffer(source.uri),
20662
- EPUB_OPEN_TIMEOUT_MS,
20663
- "epub.fetch"
20664
- );
20665
- const input = data instanceof Uint8Array ? data.buffer : data;
20666
- book = ePub(input, { openAs: "epub" });
20667
- } else {
20668
- let data = null;
20669
- if (source.kind === "text") {
20670
- const encoder = new TextEncoder();
20671
- data = encoder.encode(source.text || "").buffer;
20672
- }
20673
- const input = data instanceof Uint8Array ? data.buffer : data;
20674
- book = ePub(input, { openAs: "epub" });
20675
- }
20676
- if (book && book.opened) {
20677
- await withTimeout(book.opened, EPUB_OPEN_TIMEOUT_MS, "epub.opened");
21241
+ let data = null;
21242
+ if (source.kind === 'uri') {
21243
+ data = source.uri;
21244
+ } else if (source.kind === 'base64') {
21245
+ data = decodeBase64(source.data);
21246
+ } else if (source.kind === 'text') {
21247
+ const encoder = new TextEncoder();
21248
+ data = encoder.encode(source.text || '').buffer;
20678
21249
  }
20679
- await withTimeout(book.ready, EPUB_READY_TIMEOUT_MS, "epub.ready");
20680
- log("epub.load:ready");
21250
+
21251
+ book = ePub(data);
21252
+ await book.ready;
20681
21253
 
20682
21254
  spineItems = book.spine && book.spine.items ? book.spine.items : [];
20683
21255
  pageCount = spineItems.length;
@@ -20685,64 +21257,38 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20685
21257
 
20686
21258
  clearViewer();
20687
21259
  rendition = book.renderTo(viewer, {
20688
- width: "100%",
20689
- height: "100%",
20690
- flow: "paginated",
20691
- spread: "none",
21260
+ width: '100%',
21261
+ height: '100%',
21262
+ flow: 'paginated',
21263
+ spread: 'none',
20692
21264
  });
20693
21265
 
20694
21266
  if (rendition && rendition.hooks && rendition.hooks.content) {
20695
21267
  rendition.hooks.content.register((contents) => {
20696
- const frame =
20697
- contents && contents.document
20698
- ? contents.document.defaultView.frameElement
20699
- : null;
21268
+ const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
20700
21269
  if (frame) {
20701
- frame.setAttribute(
20702
- "sandbox",
20703
- "allow-scripts allow-same-origin"
20704
- );
21270
+ frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
20705
21271
  }
20706
21272
  });
20707
21273
  }
20708
21274
 
20709
- if (rendition && typeof rendition.on === "function") {
20710
- rendition.on("selected", (cfiRange, contents) => {
20711
- const selection =
20712
- contents && contents.window
20713
- ? contents.window.getSelection()
20714
- : null;
20715
- const text = selection ? selection.toString().trim() : "";
21275
+ if (rendition && typeof rendition.on === 'function') {
21276
+ rendition.on('selected', (cfiRange, contents) => {
21277
+ const selection = contents && contents.window ? contents.window.getSelection() : null;
21278
+ const text = selection ? selection.toString().trim() : '';
20716
21279
  if (text) {
20717
- sendEvent("TEXT_SELECTED", {
20718
- text,
20719
- pageIndex: Math.max(0, currentPage - 1),
20720
- });
21280
+ sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
20721
21281
  }
20722
- if (
20723
- rendition &&
20724
- rendition.annotations &&
20725
- typeof rendition.annotations.remove === "function"
20726
- ) {
20727
- rendition.annotations.remove(cfiRange, "highlight");
21282
+ if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
21283
+ rendition.annotations.remove(cfiRange, 'highlight');
20728
21284
  }
20729
21285
  });
20730
21286
  }
20731
21287
 
20732
- try {
20733
- await withTimeout(
20734
- displayEpubPage(0),
20735
- EPUB_DISPLAY_TIMEOUT_MS,
20736
- "epub.display"
20737
- );
20738
- log("epub.load:displayed");
20739
- } catch (err) {
20740
- reportError(err, "epub.display");
20741
- }
21288
+ await displayEpubPage(0);
20742
21289
  applyEpubZoom();
20743
21290
 
20744
21291
  const outline = await buildOutline();
20745
- log("epub.load:outline", outline.length);
20746
21292
  return { pageCount, outline };
20747
21293
  };
20748
21294
 
@@ -20757,41 +21303,34 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20757
21303
  };
20758
21304
 
20759
21305
  const getTextContent = async (pageIndex) => {
20760
- if (currentType === "text") {
20761
- const text = textPages[pageIndex] || "";
20762
- return [
20763
- {
20764
- str: text,
20765
- dir: "ltr",
20766
- width: 0,
20767
- height: 0,
20768
- transform: [1, 0, 0, 1, 0, 0],
20769
- fontName: "default",
20770
- },
20771
- ];
21306
+ if (currentType === 'text') {
21307
+ const text = textPages[pageIndex] || '';
21308
+ return [{
21309
+ str: text,
21310
+ dir: 'ltr',
21311
+ width: 0,
21312
+ height: 0,
21313
+ transform: [1, 0, 0, 1, 0, 0],
21314
+ fontName: 'default',
21315
+ }];
20772
21316
  }
20773
21317
 
20774
- if (currentType === "epub") {
21318
+ if (currentType === 'epub') {
20775
21319
  if (!book) return [];
20776
21320
  const item = spineItems[pageIndex];
20777
21321
  if (!item) return [];
20778
21322
  try {
20779
21323
  const section = book.spine.get(item.idref || item.href);
20780
- const text =
20781
- section && typeof section.text === "function"
20782
- ? await section.text()
20783
- : "";
21324
+ const text = section && typeof section.text === 'function' ? await section.text() : '';
20784
21325
  if (!text) return [];
20785
- return [
20786
- {
20787
- str: text,
20788
- dir: "ltr",
20789
- width: 0,
20790
- height: 0,
20791
- transform: [1, 0, 0, 1, 0, 0],
20792
- fontName: "default",
20793
- },
20794
- ];
21326
+ return [{
21327
+ str: text,
21328
+ dir: 'ltr',
21329
+ width: 0,
21330
+ height: 0,
21331
+ transform: [1, 0, 0, 1, 0, 0],
21332
+ fontName: 'default',
21333
+ }];
20795
21334
  } catch (err) {
20796
21335
  return [];
20797
21336
  }
@@ -20802,7 +21341,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20802
21341
 
20803
21342
  const getPageText = async (pageIndex) => {
20804
21343
  const items = await getTextContent(pageIndex);
20805
- return items.map((item) => item.str).join(" ");
21344
+ return items.map((item) => item.str).join(' ');
20806
21345
  };
20807
21346
 
20808
21347
  const searchText = async (query) => {
@@ -20818,11 +21357,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20818
21357
  while (pos !== -1) {
20819
21358
  const start = Math.max(0, pos - 20);
20820
21359
  const end = Math.min(text.length, pos + query.length + 20);
20821
- results.push({
20822
- pageIndex: i,
20823
- text: text.substring(start, end),
20824
- matchIndex,
20825
- });
21360
+ results.push({ pageIndex: i, text: text.substring(start, end), matchIndex });
20826
21361
  matchIndex += 1;
20827
21362
  pos = lower.indexOf(normalized, pos + 1);
20828
21363
  }
@@ -20838,11 +21373,11 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20838
21373
 
20839
21374
  const getPageIndex = (dest) => {
20840
21375
  if (!dest) return null;
20841
- if (typeof dest === "string") return getSpineIndexByHref(dest);
20842
- if (typeof dest !== "object") return null;
20843
- if (dest.kind === "href") return getSpineIndexByHref(dest.value);
20844
- if (dest.kind === "pageIndex") return dest.value;
20845
- if (dest.kind === "pageNumber") return Math.max(0, dest.value - 1);
21376
+ if (typeof dest === 'string') return getSpineIndexByHref(dest);
21377
+ if (typeof dest !== 'object') return null;
21378
+ if (dest.kind === 'href') return getSpineIndexByHref(dest.value);
21379
+ if (dest.kind === 'pageIndex') return dest.value;
21380
+ if (dest.kind === 'pageNumber') return Math.max(0, dest.value - 1);
20846
21381
  return null;
20847
21382
  };
20848
21383
 
@@ -20856,53 +21391,70 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20856
21391
  };
20857
21392
 
20858
21393
  const applyZoom = () => {
20859
- if (currentType === "text") {
21394
+ if (currentType === 'text') {
20860
21395
  applyTextZoom();
20861
- } else if (currentType === "epub") {
21396
+ } else if (currentType === 'epub') {
20862
21397
  applyEpubZoom();
21398
+ } else if (currentType === 'comic') {
21399
+ applyComicLayout();
20863
21400
  }
20864
21401
  };
20865
21402
 
20866
21403
  const handleCommand = async (message) => {
20867
21404
  const { id, kind, payload } = message;
20868
- log("command", kind);
20869
21405
 
20870
21406
  try {
20871
- if (kind === "load") {
21407
+ if (kind === 'load') {
20872
21408
  currentType = payload.type;
20873
21409
  zoom = 1.0;
20874
21410
 
20875
- if (currentType === "text") {
21411
+ if (currentType === 'text') {
20876
21412
  const result = await loadText(payload.source);
20877
21413
  sendState();
20878
21414
  sendResponse(id, true, result);
20879
21415
  return;
20880
21416
  }
20881
21417
 
20882
- if (currentType === "epub") {
21418
+ if (currentType === 'epub') {
20883
21419
  const result = await loadEpub(payload.source);
20884
21420
  sendState({ outline: result.outline || [] });
20885
21421
  sendResponse(id, true, result);
20886
21422
  return;
20887
21423
  }
20888
21424
 
20889
- throw new Error("Unsupported document type");
21425
+ if (currentType === 'comic') {
21426
+ const result = await loadComic(payload.source, payload.format || 'cbz');
21427
+ sendState();
21428
+ sendResponse(id, true, result);
21429
+ return;
21430
+ }
21431
+
21432
+ throw new Error('Unsupported document type');
20890
21433
  }
20891
21434
 
20892
- if (kind === "go-to-page") {
21435
+ if (kind === 'go-to-page') {
20893
21436
  const page = Math.max(1, payload.page || 1);
20894
- if (currentType === "text") {
21437
+ if (currentType === 'text') {
20895
21438
  currentPage = page;
20896
21439
  renderTextPage(page - 1);
20897
21440
  sendState();
20898
- } else if (currentType === "epub") {
21441
+ } else if (currentType === 'epub') {
20899
21442
  await displayEpubPage(page - 1);
21443
+ } else if (currentType === 'comic') {
21444
+ currentPage = page;
21445
+ const target = comicPages[page - 1];
21446
+ if (target) {
21447
+ await ensureComicPage(page - 1);
21448
+ target.scrollIntoView({ block: 'start' });
21449
+ renderComicPage(page - 1);
21450
+ sendState();
21451
+ }
20900
21452
  }
20901
21453
  sendResponse(id, true, { currentPage });
20902
21454
  return;
20903
21455
  }
20904
21456
 
20905
- if (kind === "set-zoom") {
21457
+ if (kind === 'set-zoom') {
20906
21458
  zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
20907
21459
  applyZoom();
20908
21460
  sendState();
@@ -20910,103 +21462,132 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20910
21462
  return;
20911
21463
  }
20912
21464
 
20913
- if (kind === "set-rotation") {
21465
+ if (kind === 'set-rotation') {
21466
+ rotation = ((payload.rotation || 0) % 360 + 360) % 360;
21467
+ if (currentType === 'comic') applyComicLayout();
20914
21468
  sendResponse(id, true, {});
20915
21469
  return;
20916
21470
  }
20917
21471
 
20918
- if (kind === "get-text-content") {
21472
+ if (kind === 'get-text-content') {
20919
21473
  const items = await getTextContent(payload.pageIndex || 0);
20920
21474
  sendResponse(id, true, items);
20921
21475
  return;
20922
21476
  }
20923
21477
 
20924
- if (kind === "get-page-dimensions") {
20925
- sendResponse(id, true, getPageDimensions());
21478
+ if (kind === 'get-page-dimensions') {
21479
+ if (currentType === 'comic') {
21480
+ sendResponse(
21481
+ id,
21482
+ true,
21483
+ await getComicPageDimensions(payload.pageIndex || 0)
21484
+ );
21485
+ } else {
21486
+ sendResponse(id, true, getPageDimensions());
21487
+ }
20926
21488
  return;
20927
21489
  }
20928
21490
 
20929
- if (kind === "search-text") {
20930
- const results = await searchText(payload.query || "");
21491
+ if (kind === 'get-page-preview') {
21492
+ if (currentType !== 'comic') {
21493
+ sendResponse(id, true, null);
21494
+ return;
21495
+ }
21496
+ sendResponse(
21497
+ id,
21498
+ true,
21499
+ await getComicPagePreview(payload.pageIndex || 0)
21500
+ );
21501
+ return;
21502
+ }
21503
+
21504
+ if (kind === 'search-text') {
21505
+ const results = await searchText(payload.query || '');
20931
21506
  sendResponse(id, true, results);
20932
21507
  return;
20933
21508
  }
20934
21509
 
20935
- if (kind === "select-text") {
21510
+ if (kind === 'select-text') {
20936
21511
  const selection = await selectText(payload.pageIndex || 0);
20937
21512
  sendResponse(id, true, selection);
20938
21513
  return;
20939
21514
  }
20940
21515
 
20941
- if (kind === "get-outline") {
21516
+ if (kind === 'get-outline') {
20942
21517
  const outline = await buildOutline();
20943
21518
  sendResponse(id, true, outline);
20944
21519
  return;
20945
21520
  }
20946
21521
 
20947
- if (kind === "get-page-index") {
21522
+ if (kind === 'get-page-index') {
20948
21523
  const index = getPageIndex(payload.dest);
20949
21524
  sendResponse(id, true, index);
20950
21525
  return;
20951
21526
  }
20952
21527
 
20953
- if (kind === "destroy") {
21528
+ if (kind === 'destroy') {
20954
21529
  clearViewer();
20955
21530
  sendResponse(id, true, {});
20956
21531
  return;
20957
21532
  }
20958
21533
 
20959
- sendResponse(id, false, null, "Unknown command");
21534
+ sendResponse(id, false, null, 'Unknown command');
20960
21535
  } catch (err) {
20961
- sendResponse(
20962
- id,
20963
- false,
20964
- null,
20965
- err && err.message ? err.message : String(err)
20966
- );
21536
+ sendResponse(id, false, null, err && err.message ? err.message : String(err));
20967
21537
  }
20968
21538
  };
20969
21539
 
20970
21540
  const onMessage = (event) => {
20971
21541
  let message = null;
20972
- const raw = event && typeof event === "object" ? event.data : null;
21542
+ const raw = event && typeof event === 'object' ? event.data : null;
20973
21543
 
20974
- if (raw && typeof raw === "object" && raw.kind && raw.id) {
21544
+ if (raw && typeof raw === 'object' && (raw.kind || raw.type) && raw.id) {
20975
21545
  message = raw;
20976
- } else if (typeof raw === "string") {
20977
- if (raw.startsWith("setImmediate$")) {
20978
- return;
20979
- }
21546
+ } else if (typeof raw === 'string') {
21547
+ if (raw.startsWith('setImmediate$')) return;
20980
21548
  const trimmed = raw.trim();
20981
- if (
20982
- !trimmed ||
20983
- (!trimmed.startsWith("{") && !trimmed.startsWith("["))
20984
- ) {
21549
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
20985
21550
  return;
20986
21551
  }
20987
21552
  try {
20988
21553
  message = JSON.parse(trimmed);
20989
21554
  } catch (err) {
20990
- sendEvent("RUNTIME_ERROR", {
20991
- message: "Failed to parse message",
20992
- context: "runtime.onMessage",
20993
- stack: err && err.stack ? err.stack : null,
20994
- preview: trimmed.slice(0, 200),
20995
- });
20996
21555
  return;
20997
21556
  }
20998
21557
  } else {
20999
21558
  return;
21000
21559
  }
21001
21560
 
21002
- if (!message || !message.kind || message.id == null) return;
21003
- handleCommand(message);
21561
+ if (!message || message.id == null) return;
21562
+
21563
+ if (message.type === 'asset-response') {
21564
+ const pending = runtimeAssetRequests.get(message.id);
21565
+ if (!pending) return;
21566
+ runtimeAssetRequests.delete(message.id);
21567
+ clearTimeout(pending.timeout);
21568
+ if (message.ok) pending.resolve(message.data);
21569
+ else pending.reject(new Error(message.error || 'Falha ao carregar asset CBR.'));
21570
+ return;
21571
+ }
21572
+
21573
+ if (message.type === 'file-chunk-response') {
21574
+ const pending = fileChunkRequests.get(message.id);
21575
+ if (!pending) return;
21576
+ fileChunkRequests.delete(message.id);
21577
+ clearTimeout(pending.timeout);
21578
+ if (message.ok) pending.resolve(message);
21579
+ else pending.reject(new Error(message.error || 'Falha ao ler arquivo local.'));
21580
+ return;
21581
+ }
21582
+
21583
+ if (!message.kind) return;
21584
+ handleCommand(message);
21004
21585
  };
21005
21586
 
21006
- window.addEventListener("message", onMessage);
21007
- document.addEventListener("message", onMessage);
21587
+ window.addEventListener('message', onMessage);
21588
+ document.addEventListener('message', onMessage);
21008
21589
 
21009
- sendMessage({ type: "ready" });
21590
+ sendMessage({ type: 'ready' });
21010
21591
  })();
21011
21592
  </script>
21012
21593
  </body>
@@ -21501,6 +22082,12 @@ var buildCommentTapGestureDeps = ({
21501
22082
  addAnnotationAt
21502
22083
  ];
21503
22084
 
22085
+ // components/pdfPageMetrics.ts
22086
+ var resolvePdfBasePageWidth = ({
22087
+ viewportWidth,
22088
+ horizontalPadding
22089
+ }) => Math.max(0, viewportWidth - horizontalPadding * 2);
22090
+
21504
22091
  // components/PageRenderer.tsx
21505
22092
  var import_jsx_runtime = require("react/jsx-runtime");
21506
22093
  var TEXT_MARKUP_TOOLS = /* @__PURE__ */ new Set([
@@ -22462,7 +23049,10 @@ var PageRenderer = ({
22462
23049
  }, [pageTheme]);
22463
23050
  const aspectRatio = typeof pageAspectRatio === "number" && Number.isFinite(pageAspectRatio) && pageAspectRatio > 0 ? pageAspectRatio : pageSize && pageSize.width > 0 && pageSize.height > 0 ? pageSize.width / pageSize.height : 0.77;
22464
23051
  const containerWidth = availableWidth ?? windowWidth;
22465
- const baseWidth = containerWidth * 0.92;
23052
+ const baseWidth = resolvePdfBasePageWidth({
23053
+ viewportWidth: containerWidth,
23054
+ horizontalPadding
23055
+ });
22466
23056
  const pageWidth = isNative ? baseWidth * zoom : baseWidth;
22467
23057
  const pageHeight = pageWidth / aspectRatio;
22468
23058
  const pageFrameWidth = Math.max(
@@ -23190,29 +23780,100 @@ var import_react2 = require("react");
23190
23780
  var import_react_native2 = require("react-native");
23191
23781
  var import_react_native_webview = __toESM(require("react-native-webview"));
23192
23782
  var import_core2 = require("@papyrus-sdk/core");
23783
+
23784
+ // components/webViewState.ts
23785
+ var parseWebViewState = (raw) => {
23786
+ try {
23787
+ const message = JSON.parse(raw);
23788
+ if (!message || typeof message !== "object") return null;
23789
+ const typedMessage = message;
23790
+ if (typedMessage.type !== "state") return null;
23791
+ if (!typedMessage.payload || typeof typedMessage.payload !== "object") {
23792
+ return null;
23793
+ }
23794
+ const payload = typedMessage.payload;
23795
+ const update = {};
23796
+ if (typeof payload.currentPage === "number" && Number.isInteger(payload.currentPage) && payload.currentPage >= 1) {
23797
+ update.currentPage = payload.currentPage;
23798
+ }
23799
+ if (typeof payload.pageCount === "number" && Number.isInteger(payload.pageCount) && payload.pageCount >= 0) {
23800
+ update.pageCount = payload.pageCount;
23801
+ }
23802
+ return Object.keys(update).length > 0 ? update : null;
23803
+ } catch {
23804
+ return null;
23805
+ }
23806
+ };
23807
+
23808
+ // components/WebViewViewer.tsx
23193
23809
  var import_jsx_runtime2 = require("react/jsx-runtime");
23194
23810
  var runtimeAsset = require_runtime();
23195
- var runtimeSource = (() => {
23196
- if (typeof runtimeAsset === "string") {
23197
- return { html: runtimeAsset };
23811
+ var resolveRuntimeSource = (asset) => {
23812
+ if (typeof asset === "string") {
23813
+ return { html: asset };
23198
23814
  }
23199
- if (typeof runtimeAsset === "number") {
23200
- const resolved = import_react_native2.Image.resolveAssetSource(runtimeAsset);
23815
+ if (typeof asset === "number") {
23816
+ const resolved = import_react_native2.Image.resolveAssetSource(asset);
23201
23817
  if (resolved?.uri) {
23202
23818
  return { uri: resolved.uri };
23203
23819
  }
23204
23820
  }
23205
- if (runtimeAsset && typeof runtimeAsset === "object" && "uri" in runtimeAsset) {
23206
- const uri = runtimeAsset.uri;
23821
+ if (asset && typeof asset === "object" && "uri" in asset) {
23822
+ const uri = asset.uri;
23207
23823
  if (uri) return { uri };
23208
23824
  }
23209
23825
  return { html: "" };
23210
- })();
23826
+ };
23211
23827
  var WebViewViewer = ({ engine }) => {
23212
23828
  const webViewRef = (0, import_react2.useRef)(null);
23213
23829
  const { pageTheme } = (0, import_core2.useViewerStore)();
23830
+ const bridgeEngine = engine;
23831
+ const runtimeSource = (0, import_react2.useMemo)(
23832
+ () => resolveRuntimeSource(
23833
+ bridgeEngine.getWebViewRuntimeSource?.() ?? runtimeAsset
23834
+ ),
23835
+ [bridgeEngine]
23836
+ );
23837
+ const [runtimeHtml, setRuntimeHtml] = (0, import_react2.useState)(null);
23838
+ (0, import_react2.useEffect)(() => {
23839
+ let active = true;
23840
+ if ("html" in runtimeSource) {
23841
+ setRuntimeHtml(runtimeSource.html);
23842
+ return () => {
23843
+ active = false;
23844
+ };
23845
+ }
23846
+ setRuntimeHtml(null);
23847
+ void fetch(runtimeSource.uri).then((response) => {
23848
+ if (!response.ok) throw new Error(`status ${response.status}`);
23849
+ return response.text();
23850
+ }).then((html) => {
23851
+ if (active) setRuntimeHtml(html);
23852
+ }).catch(() => {
23853
+ if (active) setRuntimeHtml(null);
23854
+ });
23855
+ return () => {
23856
+ active = false;
23857
+ };
23858
+ }, [runtimeSource]);
23859
+ const webViewSource = (0, import_react2.useMemo)(() => {
23860
+ if (runtimeHtml !== null) {
23861
+ return {
23862
+ html: runtimeHtml,
23863
+ baseUrl: "http://localhost:3005/"
23864
+ };
23865
+ }
23866
+ if ("uri" in runtimeSource) return { html: "" };
23867
+ return runtimeSource;
23868
+ }, [runtimeHtml, runtimeSource]);
23869
+ const runtimeConfig = bridgeEngine.getWebViewRuntimeConfig?.();
23870
+ const runtimeConfigScript = (0, import_react2.useMemo)(() => {
23871
+ if (!runtimeConfig) return void 0;
23872
+ return `window.__PAPYRUS_RUNTIME_CONFIG__=${JSON.stringify(
23873
+ runtimeConfig
23874
+ )};true;`;
23875
+ }, [runtimeConfig]);
23214
23876
  (0, import_react2.useEffect)(() => {
23215
- const bridgeEngine = engine;
23216
23877
  bridgeEngine.attachWebView?.({
23217
23878
  postMessage: (message) => {
23218
23879
  if (__DEV__) {
@@ -23222,9 +23883,22 @@ var WebViewViewer = ({ engine }) => {
23222
23883
  webViewRef.current?.postMessage(message);
23223
23884
  }
23224
23885
  });
23225
- }, [engine]);
23886
+ }, [bridgeEngine]);
23226
23887
  const handleMessage = (event) => {
23227
- const bridgeEngine = engine;
23888
+ const state = parseWebViewState(event.nativeEvent.data);
23889
+ if (state) {
23890
+ const viewerState = import_core2.useViewerStore.getState();
23891
+ const nextState = {};
23892
+ if (state.currentPage !== void 0 && state.currentPage !== viewerState.currentPage) {
23893
+ nextState.currentPage = state.currentPage;
23894
+ }
23895
+ if (state.pageCount !== void 0 && state.pageCount !== viewerState.pageCount) {
23896
+ nextState.pageCount = state.pageCount;
23897
+ }
23898
+ if (Object.keys(nextState).length > 0) {
23899
+ viewerState.setDocumentState(nextState);
23900
+ }
23901
+ }
23228
23902
  if (__DEV__) {
23229
23903
  console.log("[Papyrus WebView] message", event.nativeEvent.data);
23230
23904
  }
@@ -23264,11 +23938,12 @@ var WebViewViewer = ({ engine }) => {
23264
23938
  import_react_native_webview.default,
23265
23939
  {
23266
23940
  ref: webViewRef,
23267
- source: runtimeSource,
23941
+ source: webViewSource,
23268
23942
  originWhitelist: ["*"],
23269
23943
  onMessage: handleMessage,
23270
23944
  onLoadEnd: handleLoadEnd,
23271
23945
  onError: handleError,
23946
+ injectedJavaScriptBeforeContentLoaded: runtimeConfigScript,
23272
23947
  javaScriptEnabled: true,
23273
23948
  domStorageEnabled: true,
23274
23949
  scalesPageToFit: true,
@@ -24600,7 +25275,10 @@ var Viewer = ({
24600
25275
  const getPageWidthForZoom = (0, import_react4.useCallback)(
24601
25276
  (pageIndex, zoomValue) => {
24602
25277
  const safeZoom = Math.max(zoomValue, 0.25);
24603
- const baseWidth = isDouble ? columnWidth * 0.92 : windowWidth * 0.92;
25278
+ const baseWidth = resolvePdfBasePageWidth({
25279
+ viewportWidth: isDouble ? columnWidth : windowWidth,
25280
+ horizontalPadding: isDouble ? 8 : 16
25281
+ });
24604
25282
  return baseWidth * safeZoom;
24605
25283
  },
24606
25284
  [columnWidth, isDouble, windowWidth]
@@ -25133,7 +25811,10 @@ var Viewer = ({
25133
25811
  let offset = LIST_TOP_PADDING;
25134
25812
  if (isDouble) {
25135
25813
  const safeZoom2 = Math.max(zoom, 0.25);
25136
- const pageWidth2 = columnWidth * 0.92 * safeZoom2;
25814
+ const pageWidth2 = resolvePdfBasePageWidth({
25815
+ viewportWidth: columnWidth,
25816
+ horizontalPadding: 8
25817
+ }) * safeZoom2;
25137
25818
  const estimatedLength2 = pageWidth2 / DEFAULT_PAGE_ASPECT_RATIO + DOUBLE_PAGE_SPACING;
25138
25819
  for (let i = 0; i < rows.length; i += 1) {
25139
25820
  const row = rows[i];
@@ -25149,7 +25830,10 @@ var Viewer = ({
25149
25830
  return { offsets, lengths, estimatedLength: estimatedLength2 };
25150
25831
  }
25151
25832
  const safeZoom = Math.max(zoom, 0.25);
25152
- const pageWidth = windowWidth * 0.92 * safeZoom;
25833
+ const pageWidth = resolvePdfBasePageWidth({
25834
+ viewportWidth: windowWidth,
25835
+ horizontalPadding: 16
25836
+ }) * safeZoom;
25153
25837
  const estimatedLength = pageWidth / DEFAULT_PAGE_ASPECT_RATIO + CONTINUOUS_PAGE_SPACING;
25154
25838
  for (let i = 0; i < pageCount; i += 1) {
25155
25839
  const ratio = getPageAspectRatio(i);
@@ -27388,11 +28072,13 @@ var PageThumbnail = ({
27388
28072
  frameHeight,
27389
28073
  accentColor,
27390
28074
  useNativePreview,
28075
+ useImagePreview,
27391
28076
  shouldRenderPreview,
27392
28077
  onPress
27393
28078
  }) => {
27394
28079
  const viewRef = (0, import_react8.useRef)(null);
27395
28080
  const [layoutReady, setLayoutReady] = (0, import_react8.useState)(false);
28081
+ const [previewUri, setPreviewUri] = (0, import_react8.useState)(null);
27396
28082
  (0, import_react8.useEffect)(() => {
27397
28083
  if (!layoutReady || !useNativePreview || !shouldRenderPreview) return;
27398
28084
  const viewTag = (0, import_react_native9.findNodeHandle)(viewRef.current);
@@ -27408,6 +28094,24 @@ var PageThumbnail = ({
27408
28094
  useNativePreview,
27409
28095
  zoom
27410
28096
  ]);
28097
+ (0, import_react8.useEffect)(() => {
28098
+ if (!useImagePreview || !shouldRenderPreview) {
28099
+ setPreviewUri(null);
28100
+ return;
28101
+ }
28102
+ let active = true;
28103
+ const previewEngine = engine;
28104
+ const request = previewEngine.getPagePreview?.(pageIndex);
28105
+ if (!request) return;
28106
+ void request.then((uri) => {
28107
+ if (active) setPreviewUri(uri);
28108
+ }).catch(() => {
28109
+ if (active) setPreviewUri(null);
28110
+ });
28111
+ return () => {
28112
+ active = false;
28113
+ };
28114
+ }, [engine, pageIndex, shouldRenderPreview, useImagePreview]);
27411
28115
  const handleLayout = (event) => {
27412
28116
  if (event.nativeEvent.layout.width && event.nativeEvent.layout.height) {
27413
28117
  setLayoutReady(true);
@@ -27430,7 +28134,14 @@ var PageThumbnail = ({
27430
28134
  {
27431
28135
  onLayout: handleLayout,
27432
28136
  style: [styles9.thumbFrame, { width: frameWidth, height: frameHeight }],
27433
- children: useNativePreview && shouldRenderPreview ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_engine_native3.PapyrusPageView, { ref: viewRef, style: styles9.thumbView }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native9.View, { style: [styles9.thumbFallback, isDark && styles9.thumbFallbackDark], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
28137
+ children: useNativePreview && shouldRenderPreview ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_engine_native3.PapyrusPageView, { ref: viewRef, style: styles9.thumbView }) : useImagePreview && shouldRenderPreview && previewUri ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
28138
+ import_react_native9.Image,
28139
+ {
28140
+ source: { uri: previewUri },
28141
+ resizeMode: "contain",
28142
+ style: styles9.thumbImage
28143
+ }
28144
+ ) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native9.View, { style: [styles9.thumbFallback, isDark && styles9.thumbFallbackDark], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
27434
28145
  import_react_native9.Text,
27435
28146
  {
27436
28147
  style: [
@@ -27508,8 +28219,13 @@ var RightSheet = ({
27508
28219
  accentColor
27509
28220
  } = (0, import_core7.useViewerStore)();
27510
28221
  const [pagesMode, setPagesMode] = (0, import_react8.useState)(
27511
- documentType === "pdf" ? "thumbnails" : "summary"
28222
+ documentType === "pdf" || documentType === "comic" ? "thumbnails" : "summary"
27512
28223
  );
28224
+ (0, import_react8.useEffect)(() => {
28225
+ setPagesMode(
28226
+ documentType === "pdf" || documentType === "comic" ? "thumbnails" : "summary"
28227
+ );
28228
+ }, [documentType]);
27513
28229
  const isDark = uiTheme === "dark";
27514
28230
  const t = getStrings(locale);
27515
28231
  const showingNotes = sidebarRightTab === "annotations";
@@ -27527,6 +28243,7 @@ var RightSheet = ({
27527
28243
  import_react_native9.UIManager.getViewManagerConfig?.("PapyrusPageView")
27528
28244
  );
27529
28245
  const useNativePreview = renderTarget !== "webview" && hasNativePageView;
28246
+ const useImagePreview = documentType === "comic" && renderTarget === "webview" && typeof engine.getPagePreview === "function";
27530
28247
  const thumbnailDimensionsCacheRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
27531
28248
  const thumbnailDimensionsPendingRef = (0, import_react8.useRef)(/* @__PURE__ */ new Set());
27532
28249
  const thumbnailRefreshTimeoutRef = (0, import_react8.useRef)(null);
@@ -27683,7 +28400,7 @@ var RightSheet = ({
27683
28400
  );
27684
28401
  const renderThumbnailItem = (0, import_react8.useCallback)(
27685
28402
  ({ item }) => {
27686
- const shouldRenderPreview = useNativePreview && (visibleThumbnailPages.has(item) || item < resolvedThumbsPrewarmCount || Math.abs(item + 1 - currentPage) <= 1);
28403
+ const shouldRenderPreview = (useNativePreview || useImagePreview) && (visibleThumbnailPages.has(item) || item < resolvedThumbsPrewarmCount || Math.abs(item + 1 - currentPage) <= 1);
27687
28404
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
27688
28405
  PageThumbnail,
27689
28406
  {
@@ -27697,6 +28414,7 @@ var RightSheet = ({
27697
28414
  frameHeight: getThumbnailFrameHeight(item),
27698
28415
  accentColor,
27699
28416
  useNativePreview,
28417
+ useImagePreview,
27700
28418
  shouldRenderPreview,
27701
28419
  onPress: () => jumpToPage(item)
27702
28420
  }
@@ -27713,6 +28431,7 @@ var RightSheet = ({
27713
28431
  jumpToPage,
27714
28432
  resolvedThumbsPrewarmCount,
27715
28433
  useNativePreview,
28434
+ useImagePreview,
27716
28435
  visibleThumbnailPages,
27717
28436
  zoom
27718
28437
  ]
@@ -28044,6 +28763,10 @@ var styles9 = import_react_native9.StyleSheet.create({
28044
28763
  width: "100%",
28045
28764
  height: "100%"
28046
28765
  },
28766
+ thumbImage: {
28767
+ width: "100%",
28768
+ height: "100%"
28769
+ },
28047
28770
  thumbFallback: {
28048
28771
  flex: 1,
28049
28772
  alignItems: "center",