@papyrus-sdk/ui-react-native 0.2.20 → 0.2.22

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.');
20963
+ }
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
+ }
20540
21037
  }
20541
- const blob = await response.blob();
20542
- return new Promise((resolve, reject) => {
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 }));
20548
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;
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" });
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;
20675
21249
  }
20676
- if (book && book.opened) {
20677
- await withTimeout(book.opened, EPUB_OPEN_TIMEOUT_MS, "epub.opened");
20678
- }
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>
@@ -21051,6 +21632,7 @@ var import_react_native = require("react-native");
21051
21632
  var import_react_native_gesture_handler = require("react-native-gesture-handler");
21052
21633
  var import_react_native_svg = __toESM(require("react-native-svg"));
21053
21634
  var import_core = require("@papyrus-sdk/core");
21635
+ var import_types = require("@papyrus-sdk/types");
21054
21636
  var import_engine_native = require("@papyrus-sdk/engine-native");
21055
21637
 
21056
21638
  // perf/mobilePerf.ts
@@ -21507,6 +22089,14 @@ var resolvePdfBasePageWidth = ({
21507
22089
  horizontalPadding
21508
22090
  }) => Math.max(0, viewportWidth - horizontalPadding * 2);
21509
22091
 
22092
+ // components/selectionEvent.ts
22093
+ var notifyTextSelection = (handler, payload) => {
22094
+ const text = payload.text.replace(/\s+/g, " ").trim();
22095
+ if (!text) return false;
22096
+ handler?.({ ...payload, text });
22097
+ return Boolean(handler);
22098
+ };
22099
+
21510
22100
  // components/PageRenderer.tsx
21511
22101
  var import_jsx_runtime = require("react/jsx-runtime");
21512
22102
  var TEXT_MARKUP_TOOLS = /* @__PURE__ */ new Set([
@@ -21581,6 +22171,8 @@ var PageRenderer = ({
21581
22171
  horizontalPadding = 16,
21582
22172
  spacing = 24,
21583
22173
  onSelectionDragActiveChange,
22174
+ onTextSelected,
22175
+ onDefineSelection,
21584
22176
  gestureScrollLockActive = false,
21585
22177
  lastPinchEndedAt = null,
21586
22178
  requestSelectionVerticalAutoscroll
@@ -21926,6 +22518,17 @@ var PageRenderer = ({
21926
22518
  return;
21927
22519
  }
21928
22520
  const selectedText = selection.text || "";
22521
+ const normalizedText = selectedText.replace(/\s+/g, " ").trim();
22522
+ if (normalizedText) {
22523
+ import_core.papyrusEvents.emit(import_types.PapyrusEventType.TEXT_SELECTED, {
22524
+ text: normalizedText,
22525
+ pageIndex
22526
+ });
22527
+ }
22528
+ notifyTextSelection(onTextSelected, {
22529
+ text: normalizedText,
22530
+ pageIndex
22531
+ });
21929
22532
  logSelectionPerf("selection.ready", {
21930
22533
  tool: activeTool,
21931
22534
  sourceRectCount: selection.rects.length,
@@ -22861,6 +23464,20 @@ var PageRenderer = ({
22861
23464
  }
22862
23465
  ],
22863
23466
  children: [
23467
+ onDefineSelection ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
23468
+ import_react_native.Pressable,
23469
+ {
23470
+ onPress: (event) => {
23471
+ stopPressPropagation(event);
23472
+ onDefineSelection({
23473
+ text: selectionText,
23474
+ pageIndex
23475
+ });
23476
+ },
23477
+ style: [styles.selectionAction, styles.defineSelectionAction],
23478
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: styles.defineSelectionText, children: "Definir" })
23479
+ }
23480
+ ) : null,
22864
23481
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
22865
23482
  import_react_native.Pressable,
22866
23483
  {
@@ -23160,6 +23777,15 @@ var styles = import_react_native.StyleSheet.create({
23160
23777
  justifyContent: "center",
23161
23778
  marginRight: 10
23162
23779
  },
23780
+ defineSelectionAction: {
23781
+ width: 58,
23782
+ paddingHorizontal: 6
23783
+ },
23784
+ defineSelectionText: {
23785
+ color: "#FFFFFF",
23786
+ fontSize: 11,
23787
+ fontWeight: "700"
23788
+ },
23163
23789
  selectionActionLast: {
23164
23790
  marginRight: 0
23165
23791
  },
@@ -23191,7 +23817,7 @@ var styles = import_react_native.StyleSheet.create({
23191
23817
  borderRadius: 3
23192
23818
  }
23193
23819
  });
23194
- var arePageRendererPropsEqual = (previous, next) => previous.engine === next.engine && previous.pageIndex === next.pageIndex && previous.scale === next.scale && previous.PageViewComponent === next.PageViewComponent && previous.availableWidth === next.availableWidth && previous.pageViewportWidth === next.pageViewportWidth && previous.horizontalPadding === next.horizontalPadding && previous.spacing === next.spacing && previous.onSelectionDragActiveChange === next.onSelectionDragActiveChange && previous.gestureScrollLockActive === next.gestureScrollLockActive && previous.lastPinchEndedAt === next.lastPinchEndedAt && previous.requestSelectionVerticalAutoscroll === next.requestSelectionVerticalAutoscroll;
23820
+ var arePageRendererPropsEqual = (previous, next) => previous.engine === next.engine && previous.pageIndex === next.pageIndex && previous.scale === next.scale && previous.PageViewComponent === next.PageViewComponent && previous.availableWidth === next.availableWidth && previous.pageViewportWidth === next.pageViewportWidth && previous.horizontalPadding === next.horizontalPadding && previous.spacing === next.spacing && previous.onSelectionDragActiveChange === next.onSelectionDragActiveChange && previous.onTextSelected === next.onTextSelected && previous.onDefineSelection === next.onDefineSelection && previous.gestureScrollLockActive === next.gestureScrollLockActive && previous.lastPinchEndedAt === next.lastPinchEndedAt && previous.requestSelectionVerticalAutoscroll === next.requestSelectionVerticalAutoscroll;
23195
23821
  var PageRenderer_default = (0, import_react.memo)(PageRenderer, arePageRendererPropsEqual);
23196
23822
 
23197
23823
  // components/WebViewViewer.tsx
@@ -23199,29 +23825,100 @@ var import_react2 = require("react");
23199
23825
  var import_react_native2 = require("react-native");
23200
23826
  var import_react_native_webview = __toESM(require("react-native-webview"));
23201
23827
  var import_core2 = require("@papyrus-sdk/core");
23828
+
23829
+ // components/webViewState.ts
23830
+ var parseWebViewState = (raw) => {
23831
+ try {
23832
+ const message = JSON.parse(raw);
23833
+ if (!message || typeof message !== "object") return null;
23834
+ const typedMessage = message;
23835
+ if (typedMessage.type !== "state") return null;
23836
+ if (!typedMessage.payload || typeof typedMessage.payload !== "object") {
23837
+ return null;
23838
+ }
23839
+ const payload = typedMessage.payload;
23840
+ const update = {};
23841
+ if (typeof payload.currentPage === "number" && Number.isInteger(payload.currentPage) && payload.currentPage >= 1) {
23842
+ update.currentPage = payload.currentPage;
23843
+ }
23844
+ if (typeof payload.pageCount === "number" && Number.isInteger(payload.pageCount) && payload.pageCount >= 0) {
23845
+ update.pageCount = payload.pageCount;
23846
+ }
23847
+ return Object.keys(update).length > 0 ? update : null;
23848
+ } catch {
23849
+ return null;
23850
+ }
23851
+ };
23852
+
23853
+ // components/WebViewViewer.tsx
23202
23854
  var import_jsx_runtime2 = require("react/jsx-runtime");
23203
23855
  var runtimeAsset = require_runtime();
23204
- var runtimeSource = (() => {
23205
- if (typeof runtimeAsset === "string") {
23206
- return { html: runtimeAsset };
23856
+ var resolveRuntimeSource = (asset) => {
23857
+ if (typeof asset === "string") {
23858
+ return { html: asset };
23207
23859
  }
23208
- if (typeof runtimeAsset === "number") {
23209
- const resolved = import_react_native2.Image.resolveAssetSource(runtimeAsset);
23860
+ if (typeof asset === "number") {
23861
+ const resolved = import_react_native2.Image.resolveAssetSource(asset);
23210
23862
  if (resolved?.uri) {
23211
23863
  return { uri: resolved.uri };
23212
23864
  }
23213
23865
  }
23214
- if (runtimeAsset && typeof runtimeAsset === "object" && "uri" in runtimeAsset) {
23215
- const uri = runtimeAsset.uri;
23866
+ if (asset && typeof asset === "object" && "uri" in asset) {
23867
+ const uri = asset.uri;
23216
23868
  if (uri) return { uri };
23217
23869
  }
23218
23870
  return { html: "" };
23219
- })();
23871
+ };
23220
23872
  var WebViewViewer = ({ engine }) => {
23221
23873
  const webViewRef = (0, import_react2.useRef)(null);
23222
23874
  const { pageTheme } = (0, import_core2.useViewerStore)();
23875
+ const bridgeEngine = engine;
23876
+ const runtimeSource = (0, import_react2.useMemo)(
23877
+ () => resolveRuntimeSource(
23878
+ bridgeEngine.getWebViewRuntimeSource?.() ?? runtimeAsset
23879
+ ),
23880
+ [bridgeEngine]
23881
+ );
23882
+ const [runtimeHtml, setRuntimeHtml] = (0, import_react2.useState)(null);
23883
+ (0, import_react2.useEffect)(() => {
23884
+ let active = true;
23885
+ if ("html" in runtimeSource) {
23886
+ setRuntimeHtml(runtimeSource.html);
23887
+ return () => {
23888
+ active = false;
23889
+ };
23890
+ }
23891
+ setRuntimeHtml(null);
23892
+ void fetch(runtimeSource.uri).then((response) => {
23893
+ if (!response.ok) throw new Error(`status ${response.status}`);
23894
+ return response.text();
23895
+ }).then((html) => {
23896
+ if (active) setRuntimeHtml(html);
23897
+ }).catch(() => {
23898
+ if (active) setRuntimeHtml(null);
23899
+ });
23900
+ return () => {
23901
+ active = false;
23902
+ };
23903
+ }, [runtimeSource]);
23904
+ const webViewSource = (0, import_react2.useMemo)(() => {
23905
+ if (runtimeHtml !== null) {
23906
+ return {
23907
+ html: runtimeHtml,
23908
+ baseUrl: "http://localhost:3005/"
23909
+ };
23910
+ }
23911
+ if ("uri" in runtimeSource) return { html: "" };
23912
+ return runtimeSource;
23913
+ }, [runtimeHtml, runtimeSource]);
23914
+ const runtimeConfig = bridgeEngine.getWebViewRuntimeConfig?.();
23915
+ const runtimeConfigScript = (0, import_react2.useMemo)(() => {
23916
+ if (!runtimeConfig) return void 0;
23917
+ return `window.__PAPYRUS_RUNTIME_CONFIG__=${JSON.stringify(
23918
+ runtimeConfig
23919
+ )};true;`;
23920
+ }, [runtimeConfig]);
23223
23921
  (0, import_react2.useEffect)(() => {
23224
- const bridgeEngine = engine;
23225
23922
  bridgeEngine.attachWebView?.({
23226
23923
  postMessage: (message) => {
23227
23924
  if (__DEV__) {
@@ -23231,9 +23928,22 @@ var WebViewViewer = ({ engine }) => {
23231
23928
  webViewRef.current?.postMessage(message);
23232
23929
  }
23233
23930
  });
23234
- }, [engine]);
23931
+ }, [bridgeEngine]);
23235
23932
  const handleMessage = (event) => {
23236
- const bridgeEngine = engine;
23933
+ const state = parseWebViewState(event.nativeEvent.data);
23934
+ if (state) {
23935
+ const viewerState = import_core2.useViewerStore.getState();
23936
+ const nextState = {};
23937
+ if (state.currentPage !== void 0 && state.currentPage !== viewerState.currentPage) {
23938
+ nextState.currentPage = state.currentPage;
23939
+ }
23940
+ if (state.pageCount !== void 0 && state.pageCount !== viewerState.pageCount) {
23941
+ nextState.pageCount = state.pageCount;
23942
+ }
23943
+ if (Object.keys(nextState).length > 0) {
23944
+ viewerState.setDocumentState(nextState);
23945
+ }
23946
+ }
23237
23947
  if (__DEV__) {
23238
23948
  console.log("[Papyrus WebView] message", event.nativeEvent.data);
23239
23949
  }
@@ -23273,11 +23983,12 @@ var WebViewViewer = ({ engine }) => {
23273
23983
  import_react_native_webview.default,
23274
23984
  {
23275
23985
  ref: webViewRef,
23276
- source: runtimeSource,
23986
+ source: webViewSource,
23277
23987
  originWhitelist: ["*"],
23278
23988
  onMessage: handleMessage,
23279
23989
  onLoadEnd: handleLoadEnd,
23280
23990
  onError: handleError,
23991
+ injectedJavaScriptBeforeContentLoaded: runtimeConfigScript,
23281
23992
  javaScriptEnabled: true,
23282
23993
  domStorageEnabled: true,
23283
23994
  scalesPageToFit: true,
@@ -24260,7 +24971,9 @@ var Viewer = ({
24260
24971
  maxToRenderPerBatch,
24261
24972
  removeClippedSubviews,
24262
24973
  useDedicatedAndroidPdfViewer,
24263
- viewerMode
24974
+ viewerMode,
24975
+ onTextSelected,
24976
+ onDefineSelection
24264
24977
  }) => {
24265
24978
  const pageCount = (0, import_core4.useViewerStore)((state) => state.pageCount);
24266
24979
  const currentPage = (0, import_core4.useViewerStore)((state) => state.currentPage);
@@ -25396,6 +26109,8 @@ var Viewer = ({
25396
26109
  pageViewportWidth: columnWidth,
25397
26110
  spacing: DOUBLE_PAGE_SPACING,
25398
26111
  onSelectionDragActiveChange: setSelectionDragActive,
26112
+ onTextSelected,
26113
+ onDefineSelection,
25399
26114
  gestureScrollLockActive,
25400
26115
  lastPinchEndedAt,
25401
26116
  requestSelectionVerticalAutoscroll: handleSelectionVerticalAutoscroll
@@ -25412,6 +26127,8 @@ var Viewer = ({
25412
26127
  pageViewportWidth: columnWidth,
25413
26128
  spacing: DOUBLE_PAGE_SPACING,
25414
26129
  onSelectionDragActiveChange: setSelectionDragActive,
26130
+ onTextSelected,
26131
+ onDefineSelection,
25415
26132
  gestureScrollLockActive,
25416
26133
  lastPinchEndedAt,
25417
26134
  requestSelectionVerticalAutoscroll: handleSelectionVerticalAutoscroll
@@ -25431,6 +26148,8 @@ var Viewer = ({
25431
26148
  pageViewportWidth: documentSurfaceWidth,
25432
26149
  spacing: CONTINUOUS_PAGE_SPACING,
25433
26150
  onSelectionDragActiveChange: setSelectionDragActive,
26151
+ onTextSelected,
26152
+ onDefineSelection,
25434
26153
  gestureScrollLockActive,
25435
26154
  lastPinchEndedAt,
25436
26155
  requestSelectionVerticalAutoscroll: handleSelectionVerticalAutoscroll
@@ -25520,6 +26239,8 @@ var Viewer = ({
25520
26239
  pageViewportWidth: documentSurfaceWidth,
25521
26240
  spacing: 32,
25522
26241
  onSelectionDragActiveChange: setSelectionDragActive,
26242
+ onTextSelected,
26243
+ onDefineSelection,
25523
26244
  gestureScrollLockActive,
25524
26245
  lastPinchEndedAt,
25525
26246
  requestSelectionVerticalAutoscroll: handleSelectionVerticalAutoscroll
@@ -27406,11 +28127,13 @@ var PageThumbnail = ({
27406
28127
  frameHeight,
27407
28128
  accentColor,
27408
28129
  useNativePreview,
28130
+ useImagePreview,
27409
28131
  shouldRenderPreview,
27410
28132
  onPress
27411
28133
  }) => {
27412
28134
  const viewRef = (0, import_react8.useRef)(null);
27413
28135
  const [layoutReady, setLayoutReady] = (0, import_react8.useState)(false);
28136
+ const [previewUri, setPreviewUri] = (0, import_react8.useState)(null);
27414
28137
  (0, import_react8.useEffect)(() => {
27415
28138
  if (!layoutReady || !useNativePreview || !shouldRenderPreview) return;
27416
28139
  const viewTag = (0, import_react_native9.findNodeHandle)(viewRef.current);
@@ -27426,6 +28149,24 @@ var PageThumbnail = ({
27426
28149
  useNativePreview,
27427
28150
  zoom
27428
28151
  ]);
28152
+ (0, import_react8.useEffect)(() => {
28153
+ if (!useImagePreview || !shouldRenderPreview) {
28154
+ setPreviewUri(null);
28155
+ return;
28156
+ }
28157
+ let active = true;
28158
+ const previewEngine = engine;
28159
+ const request = previewEngine.getPagePreview?.(pageIndex);
28160
+ if (!request) return;
28161
+ void request.then((uri) => {
28162
+ if (active) setPreviewUri(uri);
28163
+ }).catch(() => {
28164
+ if (active) setPreviewUri(null);
28165
+ });
28166
+ return () => {
28167
+ active = false;
28168
+ };
28169
+ }, [engine, pageIndex, shouldRenderPreview, useImagePreview]);
27429
28170
  const handleLayout = (event) => {
27430
28171
  if (event.nativeEvent.layout.width && event.nativeEvent.layout.height) {
27431
28172
  setLayoutReady(true);
@@ -27448,7 +28189,14 @@ var PageThumbnail = ({
27448
28189
  {
27449
28190
  onLayout: handleLayout,
27450
28191
  style: [styles9.thumbFrame, { width: frameWidth, height: frameHeight }],
27451
- 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)(
28192
+ 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)(
28193
+ import_react_native9.Image,
28194
+ {
28195
+ source: { uri: previewUri },
28196
+ resizeMode: "contain",
28197
+ style: styles9.thumbImage
28198
+ }
28199
+ ) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native9.View, { style: [styles9.thumbFallback, isDark && styles9.thumbFallbackDark], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
27452
28200
  import_react_native9.Text,
27453
28201
  {
27454
28202
  style: [
@@ -27526,8 +28274,13 @@ var RightSheet = ({
27526
28274
  accentColor
27527
28275
  } = (0, import_core7.useViewerStore)();
27528
28276
  const [pagesMode, setPagesMode] = (0, import_react8.useState)(
27529
- documentType === "pdf" ? "thumbnails" : "summary"
28277
+ documentType === "pdf" || documentType === "comic" ? "thumbnails" : "summary"
27530
28278
  );
28279
+ (0, import_react8.useEffect)(() => {
28280
+ setPagesMode(
28281
+ documentType === "pdf" || documentType === "comic" ? "thumbnails" : "summary"
28282
+ );
28283
+ }, [documentType]);
27531
28284
  const isDark = uiTheme === "dark";
27532
28285
  const t = getStrings(locale);
27533
28286
  const showingNotes = sidebarRightTab === "annotations";
@@ -27545,6 +28298,7 @@ var RightSheet = ({
27545
28298
  import_react_native9.UIManager.getViewManagerConfig?.("PapyrusPageView")
27546
28299
  );
27547
28300
  const useNativePreview = renderTarget !== "webview" && hasNativePageView;
28301
+ const useImagePreview = documentType === "comic" && renderTarget === "webview" && typeof engine.getPagePreview === "function";
27548
28302
  const thumbnailDimensionsCacheRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
27549
28303
  const thumbnailDimensionsPendingRef = (0, import_react8.useRef)(/* @__PURE__ */ new Set());
27550
28304
  const thumbnailRefreshTimeoutRef = (0, import_react8.useRef)(null);
@@ -27701,7 +28455,7 @@ var RightSheet = ({
27701
28455
  );
27702
28456
  const renderThumbnailItem = (0, import_react8.useCallback)(
27703
28457
  ({ item }) => {
27704
- const shouldRenderPreview = useNativePreview && (visibleThumbnailPages.has(item) || item < resolvedThumbsPrewarmCount || Math.abs(item + 1 - currentPage) <= 1);
28458
+ const shouldRenderPreview = (useNativePreview || useImagePreview) && (visibleThumbnailPages.has(item) || item < resolvedThumbsPrewarmCount || Math.abs(item + 1 - currentPage) <= 1);
27705
28459
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
27706
28460
  PageThumbnail,
27707
28461
  {
@@ -27715,6 +28469,7 @@ var RightSheet = ({
27715
28469
  frameHeight: getThumbnailFrameHeight(item),
27716
28470
  accentColor,
27717
28471
  useNativePreview,
28472
+ useImagePreview,
27718
28473
  shouldRenderPreview,
27719
28474
  onPress: () => jumpToPage(item)
27720
28475
  }
@@ -27731,6 +28486,7 @@ var RightSheet = ({
27731
28486
  jumpToPage,
27732
28487
  resolvedThumbsPrewarmCount,
27733
28488
  useNativePreview,
28489
+ useImagePreview,
27734
28490
  visibleThumbnailPages,
27735
28491
  zoom
27736
28492
  ]
@@ -28062,6 +28818,10 @@ var styles9 = import_react_native9.StyleSheet.create({
28062
28818
  width: "100%",
28063
28819
  height: "100%"
28064
28820
  },
28821
+ thumbImage: {
28822
+ width: "100%",
28823
+ height: "100%"
28824
+ },
28065
28825
  thumbFallback: {
28066
28826
  flex: 1,
28067
28827
  alignItems: "center",