@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.mjs CHANGED
@@ -20395,9 +20395,121 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20395
20395
  ]).default;
20396
20396
  });
20397
20397
  </script>
20398
+ <!-- @papyrus-comic-runtime:start -->
20399
+ <script>
20400
+ (function (root, factory) {
20401
+ if (typeof module === "object" && module.exports) {
20402
+ module.exports = factory();
20403
+ } else {
20404
+ root.PapyrusComicRuntime = Object.assign(
20405
+ factory(),
20406
+ root.PapyrusComicRuntime || {}
20407
+ );
20408
+ }
20409
+ })(typeof self === "object" ? self : this, function () {
20410
+ var IMAGE_EXTENSIONS = /\\.(?:gif|jpe?g|png|svg|webp)$/i;
20411
+
20412
+ function isComicImageName(name) {
20413
+ return typeof name === "string" && IMAGE_EXTENSIONS.test(name);
20414
+ }
20415
+
20416
+ function naturalCompare(left, right) {
20417
+ var leftParts = String(left).split(/(\\d+)/);
20418
+ var rightParts = String(right).split(/(\\d+)/);
20419
+ var length = Math.max(leftParts.length, rightParts.length);
20420
+
20421
+ for (var index = 0; index < length; index += 1) {
20422
+ var leftPart = leftParts[index] || "";
20423
+ var rightPart = rightParts[index] || "";
20424
+ if (leftPart === rightPart) continue;
20425
+
20426
+ var leftNumber = /^\\d+$/.test(leftPart) ? Number(leftPart) : null;
20427
+ var rightNumber = /^\\d+$/.test(rightPart) ? Number(rightPart) : null;
20428
+ if (leftNumber !== null && rightNumber !== null) {
20429
+ return leftNumber - rightNumber;
20430
+ }
20431
+ return leftPart.localeCompare(rightPart, undefined, {
20432
+ numeric: true,
20433
+ sensitivity: "base",
20434
+ });
20435
+ }
20436
+
20437
+ return 0;
20438
+ }
20439
+
20440
+ function sortComicPageNames(names) {
20441
+ return names.slice().sort(naturalCompare);
20442
+ }
20443
+
20444
+ function patchCbrWorkerSource(workerSource, wasmUrl) {
20445
+ return workerSource.replace(
20446
+ /new URL\\("libarchive\\.wasm",import\\.meta\\.url\\)\\.href/g,
20447
+ JSON.stringify(wasmUrl)
20448
+ );
20449
+ }
20450
+
20451
+ function getComicPreviewSize(width, height, maxWidth, maxHeight) {
20452
+ var safeWidth = Math.max(1, Number(width) || 1);
20453
+ var safeHeight = Math.max(1, Number(height) || 1);
20454
+ var widthLimit = maxWidth || 240;
20455
+ var heightLimit = maxHeight || 360;
20456
+ var scale = Math.min(1, widthLimit / safeWidth, heightLimit / safeHeight);
20457
+ return {
20458
+ width: Math.max(1, Math.round(safeWidth * scale)),
20459
+ height: Math.max(1, Math.round(safeHeight * scale)),
20460
+ };
20461
+ }
20462
+
20463
+ function getComicPageAspectRatio(width, height) {
20464
+ var safeWidth = Math.max(1, Number(width) || 1);
20465
+ var safeHeight = Math.max(1, Number(height) || 1);
20466
+ return safeWidth + " / " + safeHeight;
20467
+ }
20468
+
20469
+ function getProtectedComicPageIndexes(
20470
+ currentPage,
20471
+ visiblePageIndexes,
20472
+ loadingPageIndexes
20473
+ ) {
20474
+ var protectedIndexes = new Set([
20475
+ currentPage - 1,
20476
+ currentPage,
20477
+ currentPage - 2,
20478
+ ]);
20479
+ (visiblePageIndexes || []).forEach(function (pageIndex) {
20480
+ protectedIndexes.add(pageIndex);
20481
+ });
20482
+ (loadingPageIndexes || []).forEach(function (pageIndex) {
20483
+ protectedIndexes.add(pageIndex);
20484
+ });
20485
+ return protectedIndexes;
20486
+ }
20487
+
20488
+ function isCurrentComicPageLoad(
20489
+ loadGeneration,
20490
+ currentGeneration,
20491
+ entry,
20492
+ currentEntry
20493
+ ) {
20494
+ return loadGeneration === currentGeneration && entry === currentEntry;
20495
+ }
20496
+
20497
+ return {
20498
+ isComicImageName: isComicImageName,
20499
+ sortComicPageNames: sortComicPageNames,
20500
+ patchCbrWorkerSource: patchCbrWorkerSource,
20501
+ getComicPreviewSize: getComicPreviewSize,
20502
+ getComicPageAspectRatio: getComicPageAspectRatio,
20503
+ getProtectedComicPageIndexes: getProtectedComicPageIndexes,
20504
+ isCurrentComicPageLoad: isCurrentComicPageLoad,
20505
+ };
20506
+ });
20507
+
20508
+ </script>
20509
+ <!-- @papyrus-comic-runtime:end -->
20398
20510
  <script>
20399
20511
  (function () {
20400
- const viewer = document.getElementById("viewer");
20512
+ const viewer = document.getElementById('viewer');
20401
20513
  const DEFAULT_FONT_SIZE = 16;
20402
20514
  const TEXT_PAGE_CHUNK = 1600;
20403
20515
 
@@ -20410,27 +20522,116 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20410
20522
  let currentPage = 1;
20411
20523
  let pageCount = 0;
20412
20524
  let zoom = 1.0;
20525
+ let rotation = 0;
20526
+ let comicArchive = null;
20527
+ let comicEntries = [];
20528
+ let comicContainer = null;
20529
+ let comicPages = [];
20530
+ let comicObserver = null;
20531
+ let comicScrollFrame = null;
20532
+ const comicObjectUrls = new Map();
20533
+ const comicLoading = new Map();
20534
+ const comicDimensions = new Map();
20535
+ const comicPreviewCache = new Map();
20536
+ const comicPreviewLoading = new Map();
20537
+ const COMIC_CACHE_LIMIT = 16;
20538
+ const COMIC_PREVIEW_CACHE_LIMIT = 8;
20539
+ const comicHelpers = window.PapyrusComicRuntime || {};
20540
+ const runtimeConfig = window.__PAPYRUS_RUNTIME_CONFIG__ || {};
20541
+ let comicDispose = null;
20542
+ let comicGeneration = 0;
20413
20543
 
20414
20544
  const sendMessage = (payload) => {
20415
- if (
20416
- window.ReactNativeWebView &&
20417
- window.ReactNativeWebView.postMessage
20418
- ) {
20545
+ if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
20419
20546
  window.ReactNativeWebView.postMessage(JSON.stringify(payload));
20420
20547
  return;
20421
20548
  }
20422
20549
  if (window.parent && window.parent !== window) {
20423
- window.parent.postMessage(payload, "*");
20550
+ window.parent.postMessage(payload, '*');
20424
20551
  }
20425
20552
  };
20426
20553
 
20427
20554
  const sendResponse = (id, ok, data, error) => {
20428
- sendMessage({ type: "response", id, ok, data, error });
20555
+ sendMessage({ type: 'response', id, ok, data, error });
20556
+ };
20557
+
20558
+ const runtimeAssetRequests = new Map();
20559
+ const fileChunkRequests = new Map();
20560
+
20561
+ const requestRuntimeAsset = (url, encoding) =>
20562
+ new Promise((resolve, reject) => {
20563
+ const id = \`runtime-asset-\${Date.now()}-\${Math.random()}\`;
20564
+ const timeout = setTimeout(() => {
20565
+ runtimeAssetRequests.delete(id);
20566
+ reject(new Error(\`Tempo esgotado ao carregar asset CBR: \${url}\`));
20567
+ }, 15000);
20568
+ runtimeAssetRequests.set(id, {resolve, reject, timeout});
20569
+ sendMessage({type: 'asset-request', id, url, encoding});
20570
+ });
20571
+
20572
+ const encodeArrayBufferAsDataUrl = async (buffer) => {
20573
+ const bytes = new Uint8Array(buffer);
20574
+ let binary = '';
20575
+ const chunkSize = 0x8000;
20576
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
20577
+ binary += String.fromCharCode(
20578
+ ...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),
20579
+ );
20580
+ }
20581
+ return \`data:application/wasm;base64,\${btoa(binary)}\`;
20582
+ };
20583
+
20584
+ const fetchRuntimeAsset = async (url, encoding) => {
20585
+ try {
20586
+ const response = await fetch(url);
20587
+ if (!response.ok) throw new Error(\`status \${response.status}\`);
20588
+ if (encoding === 'text') return await response.text();
20589
+ return await encodeArrayBufferAsDataUrl(await response.arrayBuffer());
20590
+ } catch {
20591
+ return await requestRuntimeAsset(url, encoding);
20592
+ }
20593
+ };
20594
+
20595
+ const requestLocalFileChunk = (uri, offset, length) =>
20596
+ new Promise((resolve, reject) => {
20597
+ const id = \`file-chunk-\${Date.now()}-\${Math.random()}\`;
20598
+ const timeout = setTimeout(() => {
20599
+ fileChunkRequests.delete(id);
20600
+ reject(new Error(\`Tempo esgotado ao ler arquivo local: \${uri}\`));
20601
+ }, 30000);
20602
+ fileChunkRequests.set(id, {resolve, reject, timeout});
20603
+ sendMessage({type: 'file-chunk-request', id, uri, offset, length});
20604
+ });
20605
+
20606
+ const readLocalFile = async (uri) => {
20607
+ const chunkSize = 1024 * 1024;
20608
+ const chunks = [];
20609
+ let offset = 0;
20610
+ let totalLength = 0;
20611
+ while (true) {
20612
+ const result = await requestLocalFileChunk(uri, offset, chunkSize);
20613
+ const bytes = decodeBase64(result.data || '');
20614
+ if (bytes.length === 0 && !result.done) {
20615
+ throw new Error('Leitura nativa retornou um chunk vazio.');
20616
+ }
20617
+ chunks.push(bytes);
20618
+ totalLength += bytes.length;
20619
+ offset += bytes.length;
20620
+ if (result.done) break;
20621
+ }
20622
+
20623
+ const output = new Uint8Array(totalLength);
20624
+ let outputOffset = 0;
20625
+ chunks.forEach((chunk) => {
20626
+ output.set(chunk, outputOffset);
20627
+ outputOffset += chunk.length;
20628
+ });
20629
+ return output.buffer;
20429
20630
  };
20430
20631
 
20431
20632
  const sendState = (extra) => {
20432
20633
  sendMessage({
20433
- type: "state",
20634
+ type: 'state',
20434
20635
  payload: {
20435
20636
  pageCount,
20436
20637
  currentPage,
@@ -20441,70 +20642,66 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20441
20642
  };
20442
20643
 
20443
20644
  const sendEvent = (name, payload) => {
20444
- sendMessage({ type: "event", name, payload });
20445
- };
20446
-
20447
- const log = (...args) => {
20448
- sendEvent("RUNTIME_LOG", {
20449
- message: args.map((value) => String(value)).join(" "),
20450
- });
20645
+ sendMessage({ type: 'event', name, payload });
20451
20646
  };
20452
20647
 
20453
20648
  const reportError = (error, context) => {
20454
- const message =
20455
- error && error.message ? error.message : String(error);
20649
+ const message = error && error.message ? error.message : String(error);
20456
20650
  const stack = error && error.stack ? error.stack : null;
20457
- sendEvent("RUNTIME_ERROR", { message, context, stack });
20458
- };
20459
-
20460
- const EPUB_OPEN_TIMEOUT_MS = 180000;
20461
- const EPUB_READY_TIMEOUT_MS = 180000;
20462
- const EPUB_DISPLAY_TIMEOUT_MS = 30000;
20463
-
20464
- const withTimeout = async (promise, ms, label) => {
20465
- let timer = null;
20466
- try {
20467
- return await Promise.race([
20468
- promise,
20469
- new Promise((_, reject) => {
20470
- timer = setTimeout(
20471
- () => reject(new Error(\`Timeout: \${label}\`)),
20472
- ms
20473
- );
20474
- }),
20475
- ]);
20476
- } finally {
20477
- if (timer) {
20478
- clearTimeout(timer);
20479
- }
20480
- }
20651
+ sendEvent('RUNTIME_ERROR', { message, context, stack });
20481
20652
  };
20482
20653
 
20483
20654
  const shouldIgnoreError = (error) => {
20484
- const message =
20485
- error && error.message ? error.message : String(error || "");
20486
- return message.includes("ResizeObserver loop");
20655
+ const message = error && error.message ? error.message : String(error || '');
20656
+ return message.includes('ResizeObserver loop');
20487
20657
  };
20488
20658
 
20489
- window.addEventListener("error", (event) => {
20659
+ window.addEventListener('error', (event) => {
20490
20660
  const error = event.error || event.message;
20491
20661
  if (shouldIgnoreError(error)) return;
20492
- reportError(error, "window.error");
20662
+ reportError(error, 'window.error');
20493
20663
  });
20494
20664
 
20495
- window.addEventListener("unhandledrejection", (event) => {
20665
+ window.addEventListener('unhandledrejection', (event) => {
20496
20666
  if (shouldIgnoreError(event.reason)) return;
20497
- reportError(event.reason, "unhandledrejection");
20667
+ reportError(event.reason, 'unhandledrejection');
20498
20668
  });
20499
20669
 
20670
+ const clearComicState = () => {
20671
+ comicGeneration += 1;
20672
+ if (comicObserver) {
20673
+ comicObserver.disconnect();
20674
+ comicObserver = null;
20675
+ }
20676
+ if (comicScrollFrame !== null) {
20677
+ cancelAnimationFrame(comicScrollFrame);
20678
+ comicScrollFrame = null;
20679
+ }
20680
+ comicObjectUrls.forEach((url) => URL.revokeObjectURL(url));
20681
+ comicObjectUrls.clear();
20682
+ comicLoading.clear();
20683
+ comicDimensions.clear();
20684
+ comicPreviewCache.clear();
20685
+ comicPreviewLoading.clear();
20686
+ if (comicDispose) {
20687
+ void comicDispose();
20688
+ comicDispose = null;
20689
+ }
20690
+ comicArchive = null;
20691
+ comicEntries = [];
20692
+ comicContainer = null;
20693
+ comicPages = [];
20694
+ };
20695
+
20500
20696
  const clearViewer = () => {
20697
+ clearComicState();
20501
20698
  while (viewer.firstChild) {
20502
20699
  viewer.removeChild(viewer.firstChild);
20503
20700
  }
20504
20701
  };
20505
20702
 
20506
20703
  const decodeBase64 = (value) => {
20507
- const clean = value.replace(/\\s/g, "");
20704
+ const clean = value.replace(/\\s/g, '');
20508
20705
  const binary = atob(clean);
20509
20706
  const len = binary.length;
20510
20707
  const bytes = new Uint8Array(len);
@@ -20516,41 +20713,438 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20516
20713
 
20517
20714
  const decodeBase64ToText = (value) => {
20518
20715
  const bytes = decodeBase64(value);
20519
- return new TextDecoder("utf-8").decode(bytes);
20716
+ return new TextDecoder('utf-8').decode(bytes);
20520
20717
  };
20521
20718
 
20522
- const fetchUriAsArrayBuffer = async (uri) => {
20523
- const response = await fetch(uri);
20524
- const ok = response.ok || response.status === 0;
20525
- if (!ok) {
20526
- throw new Error(
20527
- "Failed to fetch epub uri (" + response.status + ")"
20719
+ const sourceToArrayBuffer = async (source) => {
20720
+ if (source.kind === 'uri') {
20721
+ if (/^(?:file|content):\\/\\//i.test(source.uri)) {
20722
+ return readLocalFile(source.uri);
20723
+ }
20724
+ const response = await fetch(source.uri);
20725
+ if (!response.ok && response.status !== 0) {
20726
+ throw new Error(\`Falha ao carregar quadrinho (\${response.status})\`);
20727
+ }
20728
+ return response.arrayBuffer();
20729
+ }
20730
+ if (source.kind === 'base64') {
20731
+ return decodeBase64(source.data).buffer;
20732
+ }
20733
+ throw new Error('Fonte de quadrinho inv\xE1lida.');
20734
+ };
20735
+
20736
+ const comicMimeType = (name) => {
20737
+ const extension = name.split('.').pop()?.toLowerCase();
20738
+ if (extension === 'png') return 'image/png';
20739
+ if (extension === 'gif') return 'image/gif';
20740
+ if (extension === 'svg') return 'image/svg+xml';
20741
+ if (extension === 'webp') return 'image/webp';
20742
+ return 'image/jpeg';
20743
+ };
20744
+
20745
+ const updateComicCurrentPage = () => {
20746
+ if (!comicContainer || comicPages.length === 0) return;
20747
+ const viewportTop = comicContainer.scrollTop;
20748
+ let closestIndex = currentPage - 1;
20749
+ let closestDistance = Number.POSITIVE_INFINITY;
20750
+ comicPages.forEach((page, index) => {
20751
+ const distance = Math.abs(page.offsetTop - viewportTop);
20752
+ if (distance < closestDistance) {
20753
+ closestDistance = distance;
20754
+ closestIndex = index;
20755
+ }
20756
+ });
20757
+ const nextPage = Math.max(1, Math.min(pageCount, closestIndex + 1));
20758
+ if (nextPage !== currentPage) {
20759
+ currentPage = nextPage;
20760
+ sendState();
20761
+ }
20762
+ };
20763
+
20764
+ const scheduleComicCurrentPage = () => {
20765
+ if (comicScrollFrame !== null) return;
20766
+ comicScrollFrame = requestAnimationFrame(() => {
20767
+ comicScrollFrame = null;
20768
+ updateComicCurrentPage();
20769
+ });
20770
+ };
20771
+
20772
+ const evictComicUrls = () => {
20773
+ if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) return;
20774
+ const protectedIndexes = new Set([
20775
+ currentPage - 1,
20776
+ currentPage,
20777
+ currentPage - 2,
20778
+ ]);
20779
+ for (const [index, url] of comicObjectUrls) {
20780
+ if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) break;
20781
+ if (protectedIndexes.has(index)) continue;
20782
+ URL.revokeObjectURL(url);
20783
+ comicObjectUrls.delete(index);
20784
+ const image = comicPages[index]?.querySelector('img');
20785
+ const page = comicPages[index];
20786
+ const dimensions = comicDimensions.get(index);
20787
+ const width = dimensions?.width || image?.naturalWidth || 0;
20788
+ const height = dimensions?.height || image?.naturalHeight || 0;
20789
+ if (page && width > 0 && height > 0) {
20790
+ page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
20791
+ width,
20792
+ height
20793
+ );
20794
+ }
20795
+ if (image) image.removeAttribute('src');
20796
+ }
20797
+ };
20798
+
20799
+ const ensureComicPage = async (pageIndex) => {
20800
+ if (comicObjectUrls.has(pageIndex)) {
20801
+ const image = comicPages[pageIndex]?.querySelector('img');
20802
+ if (image && !image.src) image.src = comicObjectUrls.get(pageIndex);
20803
+ return comicObjectUrls.get(pageIndex);
20804
+ }
20805
+ if (comicLoading.has(pageIndex)) return comicLoading.get(pageIndex);
20806
+
20807
+ const entry = comicEntries[pageIndex];
20808
+ if (!entry) throw new Error(\`P\xE1gina de quadrinho inv\xE1lida: \${pageIndex}\`);
20809
+ const loadGeneration = comicGeneration;
20810
+ const pending = entry.async('blob').then((blob) => {
20811
+ if (
20812
+ !comicHelpers.isCurrentComicPageLoad(
20813
+ loadGeneration,
20814
+ comicGeneration,
20815
+ entry,
20816
+ comicEntries[pageIndex]
20817
+ )
20818
+ ) {
20819
+ return null;
20820
+ }
20821
+ const url = URL.createObjectURL(
20822
+ new Blob([blob], { type: comicMimeType(entry.name) })
20528
20823
  );
20824
+ comicObjectUrls.set(pageIndex, url);
20825
+ const image = comicPages[pageIndex]?.querySelector('img');
20826
+ if (image) image.src = url;
20827
+ evictComicUrls();
20828
+ return url;
20829
+ });
20830
+ comicLoading.set(pageIndex, pending);
20831
+ try {
20832
+ return await pending;
20833
+ } finally {
20834
+ if (comicLoading.get(pageIndex) === pending) {
20835
+ comicLoading.delete(pageIndex);
20836
+ }
20529
20837
  }
20530
- if (typeof response.arrayBuffer === "function") {
20531
- return response.arrayBuffer();
20838
+ };
20839
+
20840
+ const renderComicPage = (pageIndex) => {
20841
+ const image = comicPages[pageIndex]?.querySelector('img');
20842
+ if (!image) return;
20843
+ image.style.width = \`\${Math.max(40, Math.round(100 * zoom))}%\`;
20844
+ image.style.transform = \`rotate(\${rotation}deg)\`;
20845
+ };
20846
+
20847
+ const applyComicLayout = () => {
20848
+ comicPages.forEach((_, pageIndex) => renderComicPage(pageIndex));
20849
+ };
20850
+
20851
+ const loadComic = async (source, format) => {
20852
+ clearViewer();
20853
+ let entries;
20854
+ if (format === 'cbr') {
20855
+ const clientUrl = runtimeConfig.cbrClientUrl;
20856
+ const workerUrl = runtimeConfig.cbrWorkerUrl;
20857
+ const wasmUrl = runtimeConfig.cbrWasmUrl;
20858
+ if (!clientUrl || !workerUrl || !wasmUrl) {
20859
+ throw new Error(
20860
+ 'CBR no mobile exige o pacote opcional de runtime libarchive.'
20861
+ );
20862
+ }
20863
+
20864
+ const [clientSource, workerSource, wasmSource] = await Promise.all([
20865
+ fetchRuntimeAsset(clientUrl, 'text'),
20866
+ fetchRuntimeAsset(workerUrl, 'text'),
20867
+ runtimeConfig.cbrWasmDataUrl ||
20868
+ fetchRuntimeAsset(wasmUrl, 'base64'),
20869
+ ]);
20870
+ const patchedWorkerSource = comicHelpers.patchCbrWorkerSource(
20871
+ workerSource,
20872
+ wasmSource
20873
+ );
20874
+ const workerObjectUrl = URL.createObjectURL(
20875
+ new Blob([patchedWorkerSource], {type: 'text/javascript'})
20876
+ );
20877
+ const clientObjectUrl = URL.createObjectURL(
20878
+ new Blob([clientSource], { type: 'text/javascript' })
20879
+ );
20880
+ try {
20881
+ const archiveModule = await import(clientObjectUrl);
20882
+ const createCbrWorker = () => {
20883
+ const worker = new Worker(workerObjectUrl, {type: 'module'});
20884
+ worker.addEventListener('error', (event) => {
20885
+ sendEvent('RUNTIME_ERROR', {
20886
+ message: \`CBR worker: \${event?.message || 'falha ao executar o worker'}\`,
20887
+ context: 'comic.worker',
20888
+ });
20889
+ });
20890
+ worker.addEventListener('messageerror', () => {
20891
+ sendEvent('RUNTIME_ERROR', {
20892
+ message: 'CBR worker: falha ao transferir mensagem',
20893
+ context: 'comic.worker',
20894
+ });
20895
+ });
20896
+ return worker;
20897
+ };
20898
+ archiveModule.Archive.init({getWorker: createCbrWorker});
20899
+ const bytes = await sourceToArrayBuffer(source);
20900
+ const file = new Blob([bytes], {
20901
+ type: 'application/vnd.comicbook-rar',
20902
+ });
20903
+ const archive = await archiveModule.Archive.open(file);
20904
+ const files = await archive.getFilesArray();
20905
+ entries = files
20906
+ .filter(
20907
+ (item) => {
20908
+ const name = \`\${item?.path || ''}\${item?.file?.name || ''}\`;
20909
+ return item?.file && comicHelpers.isComicImageName(name);
20910
+ }
20911
+ )
20912
+ .map((item) => {
20913
+ const name = \`\${item.path || ''}\${item.file.name}\`;
20914
+ return {
20915
+ name,
20916
+ async: async () => item.file.extract(),
20917
+ };
20918
+ });
20919
+ comicDispose = async () => {
20920
+ await archive.close();
20921
+ URL.revokeObjectURL(workerObjectUrl);
20922
+ URL.revokeObjectURL(clientObjectUrl);
20923
+ };
20924
+ } catch (error) {
20925
+ URL.revokeObjectURL(workerObjectUrl);
20926
+ URL.revokeObjectURL(clientObjectUrl);
20927
+ throw error;
20928
+ }
20929
+ } else {
20930
+ if (!window.JSZip) {
20931
+ throw new Error('O runtime ZIP do leitor mobile n\xE3o foi carregado.');
20932
+ }
20933
+
20934
+ const bytes = await sourceToArrayBuffer(source);
20935
+ comicArchive = await window.JSZip.loadAsync(bytes);
20936
+ entries = Object.keys(comicArchive.files)
20937
+ .filter((name) => {
20938
+ const entry = comicArchive.files[name];
20939
+ return !entry.dir && comicHelpers.isComicImageName(name);
20940
+ })
20941
+ .map((name) => comicArchive.files[name]);
20532
20942
  }
20533
- const blob = await response.blob();
20534
- return new Promise((resolve, reject) => {
20943
+
20944
+ const names = comicHelpers.sortComicPageNames(
20945
+ entries.map((entry) => entry.name)
20946
+ );
20947
+ const entriesByName = new Map(entries.map((entry) => [entry.name, entry]));
20948
+ comicEntries = names.map((name) => entriesByName.get(name));
20949
+ if (comicEntries.length === 0) {
20950
+ if (comicDispose) {
20951
+ await comicDispose();
20952
+ comicDispose = null;
20953
+ }
20954
+ throw new Error('O arquivo n\xE3o cont\xE9m p\xE1ginas de imagem.');
20955
+ }
20956
+
20957
+ comicContainer = document.createElement('div');
20958
+ comicContainer.style.width = '100%';
20959
+ comicContainer.style.height = '100%';
20960
+ comicContainer.style.overflow = 'auto';
20961
+ comicContainer.style.padding = '16px 0 120px';
20962
+ comicContainer.style.boxSizing = 'border-box';
20963
+ comicContainer.style.background = '#ffffff';
20964
+ comicContainer.addEventListener('scroll', scheduleComicCurrentPage, {
20965
+ passive: true,
20966
+ });
20967
+ viewer.style.overflow = 'hidden';
20968
+ viewer.appendChild(comicContainer);
20969
+
20970
+ comicPages = comicEntries.map((entry, pageIndex) => {
20971
+ const page = document.createElement('div');
20972
+ page.dataset.pageIndex = String(pageIndex);
20973
+ page.style.width = '100%';
20974
+ page.style.minHeight = '80px';
20975
+ page.style.display = 'flex';
20976
+ page.style.justifyContent = 'center';
20977
+ page.style.alignItems = 'flex-start';
20978
+ page.style.padding = '0 12px 20px';
20979
+ page.style.boxSizing = 'border-box';
20980
+
20981
+ const image = document.createElement('img');
20982
+ image.alt = \`P\xE1gina \${pageIndex + 1}\`;
20983
+ image.draggable = false;
20984
+ image.decoding = 'async';
20985
+ image.loading = 'lazy';
20986
+ image.style.display = 'block';
20987
+ image.style.maxWidth = 'none';
20988
+ image.style.height = 'auto';
20989
+ image.style.userSelect = 'none';
20990
+ image.addEventListener('load', () => {
20991
+ if (image.naturalWidth > 0 && image.naturalHeight > 0) {
20992
+ comicDimensions.set(pageIndex, {
20993
+ width: image.naturalWidth,
20994
+ height: image.naturalHeight,
20995
+ });
20996
+ page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
20997
+ image.naturalWidth,
20998
+ image.naturalHeight
20999
+ );
21000
+ }
21001
+ });
21002
+ page.appendChild(image);
21003
+ comicContainer.appendChild(page);
21004
+ return page;
21005
+ });
21006
+
21007
+ if (typeof IntersectionObserver === 'function') {
21008
+ comicObserver = new IntersectionObserver(
21009
+ (observations) => {
21010
+ observations.forEach((observation) => {
21011
+ if (!observation.isIntersecting) return;
21012
+ const pageIndex = Number(observation.target.dataset.pageIndex);
21013
+ void ensureComicPage(pageIndex).catch((error) =>
21014
+ sendEvent('RUNTIME_ERROR', {
21015
+ message: error?.message || String(error),
21016
+ context: 'comic.page',
21017
+ })
21018
+ );
21019
+ });
21020
+ scheduleComicCurrentPage();
21021
+ },
21022
+ { root: comicContainer, rootMargin: '600px 0px' }
21023
+ );
21024
+ comicPages.forEach((page) => comicObserver.observe(page));
21025
+ } else {
21026
+ for (let pageIndex = 0; pageIndex < Math.min(3, comicPages.length); pageIndex += 1) {
21027
+ void ensureComicPage(pageIndex);
21028
+ }
21029
+ }
21030
+ pageCount = comicPages.length;
21031
+ currentPage = 1;
21032
+ await ensureComicPage(0);
21033
+ applyComicLayout();
21034
+ return { pageCount };
21035
+ };
21036
+
21037
+ const readComicBlobAsDataUrl = (blob, mimeType) =>
21038
+ new Promise((resolve, reject) => {
20535
21039
  const reader = new FileReader();
20536
- reader.onerror = () =>
20537
- reject(new Error("Failed to read epub blob"));
21040
+ reader.onerror = () => reject(new Error('Falha ao gerar preview.'));
20538
21041
  reader.onload = () => resolve(reader.result);
20539
- reader.readAsArrayBuffer(blob);
21042
+ reader.readAsDataURL(new Blob([blob], { type: mimeType }));
20540
21043
  });
21044
+
21045
+ const createComicPagePreview = async (pageIndex) => {
21046
+ const entry = comicEntries[pageIndex];
21047
+ if (!entry) return null;
21048
+ const mimeType = comicMimeType(entry.name);
21049
+ const sourceUrl = await ensureComicPage(pageIndex);
21050
+ if (!sourceUrl) return null;
21051
+ try {
21052
+ const image = new Image();
21053
+ image.decoding = 'async';
21054
+ await new Promise((resolve, reject) => {
21055
+ image.onload = resolve;
21056
+ image.onerror = () => reject(new Error('Falha ao decodificar preview.'));
21057
+ image.src = sourceUrl;
21058
+ });
21059
+ const size = comicHelpers.getComicPreviewSize(
21060
+ image.naturalWidth || image.width,
21061
+ image.naturalHeight || image.height
21062
+ );
21063
+ const canvas = document.createElement('canvas');
21064
+ canvas.width = size.width;
21065
+ canvas.height = size.height;
21066
+ const context = canvas.getContext('2d');
21067
+ if (!context) throw new Error('Canvas indispon\xEDvel para preview.');
21068
+ context.fillStyle = '#ffffff';
21069
+ context.fillRect(0, 0, size.width, size.height);
21070
+ context.drawImage(image, 0, 0, size.width, size.height);
21071
+ return canvas.toDataURL('image/jpeg', 0.78);
21072
+ } catch {
21073
+ try {
21074
+ const response = await fetch(sourceUrl);
21075
+ const blob = await response.blob();
21076
+ return await readComicBlobAsDataUrl(blob, mimeType);
21077
+ } catch {
21078
+ return null;
21079
+ }
21080
+ }
21081
+ };
21082
+
21083
+ const getComicPagePreview = async (pageIndex) => {
21084
+ const entry = comicEntries[pageIndex];
21085
+ if (!entry) return null;
21086
+ if (comicPreviewCache.has(pageIndex)) {
21087
+ return comicPreviewCache.get(pageIndex);
21088
+ }
21089
+ if (comicPreviewLoading.has(pageIndex)) {
21090
+ return comicPreviewLoading.get(pageIndex);
21091
+ }
21092
+ const loadGeneration = comicGeneration;
21093
+ const pending = createComicPagePreview(pageIndex).then((preview) => {
21094
+ if (
21095
+ !comicHelpers.isCurrentComicPageLoad(
21096
+ loadGeneration,
21097
+ comicGeneration,
21098
+ entry,
21099
+ comicEntries[pageIndex]
21100
+ )
21101
+ ) {
21102
+ return null;
21103
+ }
21104
+ comicPreviewCache.set(pageIndex, preview);
21105
+ while (comicPreviewCache.size > COMIC_PREVIEW_CACHE_LIMIT) {
21106
+ comicPreviewCache.delete(comicPreviewCache.keys().next().value);
21107
+ }
21108
+ return preview;
21109
+ });
21110
+ comicPreviewLoading.set(pageIndex, pending);
21111
+ try {
21112
+ return await pending;
21113
+ } finally {
21114
+ if (comicPreviewLoading.get(pageIndex) === pending) {
21115
+ comicPreviewLoading.delete(pageIndex);
21116
+ }
21117
+ }
21118
+ };
21119
+
21120
+ const getComicPageDimensions = async (pageIndex) => {
21121
+ const known = comicDimensions.get(pageIndex);
21122
+ if (known) return known;
21123
+ await ensureComicPage(pageIndex);
21124
+ const image = comicPages[pageIndex]?.querySelector('img');
21125
+ if (image && !image.complete) {
21126
+ await new Promise((resolve) => {
21127
+ image.addEventListener('load', resolve, { once: true });
21128
+ image.addEventListener('error', resolve, { once: true });
21129
+ });
21130
+ }
21131
+ return (
21132
+ comicDimensions.get(pageIndex) || {
21133
+ width: image?.naturalWidth || 0,
21134
+ height: image?.naturalHeight || 0,
21135
+ }
21136
+ );
20541
21137
  };
20542
21138
 
20543
21139
  const normalizeHref = (href) => {
20544
- if (!href) return "";
20545
- return href.split("#")[0];
21140
+ if (!href) return '';
21141
+ return href.split('#')[0];
20546
21142
  };
20547
21143
 
20548
21144
  const getSpineIndexByHref = (href) => {
20549
21145
  const normalized = normalizeHref(href);
20550
21146
  if (!normalized) return -1;
20551
- return spineItems.findIndex(
20552
- (item) => normalizeHref(item.href) === normalized
20553
- );
21147
+ return spineItems.findIndex((item) => normalizeHref(item.href) === normalized);
20554
21148
  };
20555
21149
 
20556
21150
  const buildOutline = async () => {
@@ -20559,11 +21153,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20559
21153
  const toc = nav && nav.toc ? nav.toc : [];
20560
21154
 
20561
21155
  const mapItem = (item) => {
20562
- const title = item.label || item.title || "";
20563
- const pageIndex = getSpineIndexByHref(item.href || "");
20564
- const children = Array.isArray(item.subitems)
20565
- ? item.subitems.map(mapItem)
20566
- : [];
21156
+ const title = item.label || item.title || '';
21157
+ const pageIndex = getSpineIndexByHref(item.href || '');
21158
+ const children = Array.isArray(item.subitems) ? item.subitems.map(mapItem) : [];
20567
21159
  const outlineItem = { title, pageIndex };
20568
21160
  if (children.length > 0) outlineItem.children = children;
20569
21161
  return outlineItem;
@@ -20575,10 +21167,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20575
21167
  const applyEpubZoom = () => {
20576
21168
  if (!rendition || !rendition.themes) return;
20577
21169
  const fontSize = \`\${Math.round(zoom * 100)}%\`;
20578
- if (typeof rendition.themes.fontSize === "function") {
21170
+ if (typeof rendition.themes.fontSize === 'function') {
20579
21171
  rendition.themes.fontSize(fontSize);
20580
- } else if (typeof rendition.themes.override === "function") {
20581
- rendition.themes.override("font-size", fontSize);
21172
+ } else if (typeof rendition.themes.override === 'function') {
21173
+ rendition.themes.override('font-size', fontSize);
20582
21174
  }
20583
21175
  };
20584
21176
 
@@ -20589,14 +21181,13 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20589
21181
  };
20590
21182
 
20591
21183
  const renderTextPage = (pageIndex) => {
20592
- const text = textPages[pageIndex] || "";
21184
+ const text = textPages[pageIndex] || '';
20593
21185
  if (!textContainer) {
20594
- textContainer = document.createElement("div");
20595
- textContainer.style.padding = "24px";
20596
- textContainer.style.lineHeight = "1.6";
20597
- textContainer.style.whiteSpace = "pre-wrap";
20598
- textContainer.style.fontFamily =
20599
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
21186
+ textContainer = document.createElement('div');
21187
+ textContainer.style.padding = '24px';
21188
+ textContainer.style.lineHeight = '1.6';
21189
+ textContainer.style.whiteSpace = 'pre-wrap';
21190
+ textContainer.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
20600
21191
  viewer.appendChild(textContainer);
20601
21192
  }
20602
21193
  textContainer.textContent = text;
@@ -20608,18 +21199,18 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20608
21199
  for (let i = 0; i < text.length; i += TEXT_PAGE_CHUNK) {
20609
21200
  pages.push(text.slice(i, i + TEXT_PAGE_CHUNK));
20610
21201
  }
20611
- return pages.length > 0 ? pages : [""];
21202
+ return pages.length > 0 ? pages : [''];
20612
21203
  };
20613
21204
 
20614
21205
  const loadText = async (source) => {
20615
- let text = "";
20616
- if (source.kind === "uri") {
21206
+ let text = '';
21207
+ if (source.kind === 'uri') {
20617
21208
  const res = await fetch(source.uri);
20618
21209
  text = await res.text();
20619
- } else if (source.kind === "base64") {
21210
+ } else if (source.kind === 'base64') {
20620
21211
  text = decodeBase64ToText(source.data);
20621
- } else if (source.kind === "text") {
20622
- text = source.text || "";
21212
+ } else if (source.kind === 'text') {
21213
+ text = source.text || '';
20623
21214
  }
20624
21215
 
20625
21216
  clearViewer();
@@ -20632,44 +21223,25 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20632
21223
  };
20633
21224
 
20634
21225
  const loadEpub = async (source) => {
20635
- log("epub.load:start", source.kind);
20636
- if (rendition && typeof rendition.destroy === "function") {
21226
+ if (rendition && typeof rendition.destroy === 'function') {
20637
21227
  rendition.destroy();
20638
21228
  }
20639
- if (book && typeof book.destroy === "function") {
21229
+ if (book && typeof book.destroy === 'function') {
20640
21230
  book.destroy();
20641
21231
  }
20642
21232
 
20643
- if (source.kind === "base64") {
20644
- book = ePub();
20645
- await withTimeout(
20646
- book.open(source.data, "base64"),
20647
- EPUB_OPEN_TIMEOUT_MS,
20648
- "epub.open"
20649
- );
20650
- } else if (source.kind === "uri") {
20651
- log("epub.load:fetch-uri");
20652
- const data = await withTimeout(
20653
- fetchUriAsArrayBuffer(source.uri),
20654
- EPUB_OPEN_TIMEOUT_MS,
20655
- "epub.fetch"
20656
- );
20657
- const input = data instanceof Uint8Array ? data.buffer : data;
20658
- book = ePub(input, { openAs: "epub" });
20659
- } else {
20660
- let data = null;
20661
- if (source.kind === "text") {
20662
- const encoder = new TextEncoder();
20663
- data = encoder.encode(source.text || "").buffer;
20664
- }
20665
- const input = data instanceof Uint8Array ? data.buffer : data;
20666
- book = ePub(input, { openAs: "epub" });
21233
+ let data = null;
21234
+ if (source.kind === 'uri') {
21235
+ data = source.uri;
21236
+ } else if (source.kind === 'base64') {
21237
+ data = decodeBase64(source.data);
21238
+ } else if (source.kind === 'text') {
21239
+ const encoder = new TextEncoder();
21240
+ data = encoder.encode(source.text || '').buffer;
20667
21241
  }
20668
- if (book && book.opened) {
20669
- await withTimeout(book.opened, EPUB_OPEN_TIMEOUT_MS, "epub.opened");
20670
- }
20671
- await withTimeout(book.ready, EPUB_READY_TIMEOUT_MS, "epub.ready");
20672
- log("epub.load:ready");
21242
+
21243
+ book = ePub(data);
21244
+ await book.ready;
20673
21245
 
20674
21246
  spineItems = book.spine && book.spine.items ? book.spine.items : [];
20675
21247
  pageCount = spineItems.length;
@@ -20677,64 +21249,38 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20677
21249
 
20678
21250
  clearViewer();
20679
21251
  rendition = book.renderTo(viewer, {
20680
- width: "100%",
20681
- height: "100%",
20682
- flow: "paginated",
20683
- spread: "none",
21252
+ width: '100%',
21253
+ height: '100%',
21254
+ flow: 'paginated',
21255
+ spread: 'none',
20684
21256
  });
20685
21257
 
20686
21258
  if (rendition && rendition.hooks && rendition.hooks.content) {
20687
21259
  rendition.hooks.content.register((contents) => {
20688
- const frame =
20689
- contents && contents.document
20690
- ? contents.document.defaultView.frameElement
20691
- : null;
21260
+ const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
20692
21261
  if (frame) {
20693
- frame.setAttribute(
20694
- "sandbox",
20695
- "allow-scripts allow-same-origin"
20696
- );
21262
+ frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
20697
21263
  }
20698
21264
  });
20699
21265
  }
20700
21266
 
20701
- if (rendition && typeof rendition.on === "function") {
20702
- rendition.on("selected", (cfiRange, contents) => {
20703
- const selection =
20704
- contents && contents.window
20705
- ? contents.window.getSelection()
20706
- : null;
20707
- const text = selection ? selection.toString().trim() : "";
21267
+ if (rendition && typeof rendition.on === 'function') {
21268
+ rendition.on('selected', (cfiRange, contents) => {
21269
+ const selection = contents && contents.window ? contents.window.getSelection() : null;
21270
+ const text = selection ? selection.toString().trim() : '';
20708
21271
  if (text) {
20709
- sendEvent("TEXT_SELECTED", {
20710
- text,
20711
- pageIndex: Math.max(0, currentPage - 1),
20712
- });
21272
+ sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
20713
21273
  }
20714
- if (
20715
- rendition &&
20716
- rendition.annotations &&
20717
- typeof rendition.annotations.remove === "function"
20718
- ) {
20719
- rendition.annotations.remove(cfiRange, "highlight");
21274
+ if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
21275
+ rendition.annotations.remove(cfiRange, 'highlight');
20720
21276
  }
20721
21277
  });
20722
21278
  }
20723
21279
 
20724
- try {
20725
- await withTimeout(
20726
- displayEpubPage(0),
20727
- EPUB_DISPLAY_TIMEOUT_MS,
20728
- "epub.display"
20729
- );
20730
- log("epub.load:displayed");
20731
- } catch (err) {
20732
- reportError(err, "epub.display");
20733
- }
21280
+ await displayEpubPage(0);
20734
21281
  applyEpubZoom();
20735
21282
 
20736
21283
  const outline = await buildOutline();
20737
- log("epub.load:outline", outline.length);
20738
21284
  return { pageCount, outline };
20739
21285
  };
20740
21286
 
@@ -20749,41 +21295,34 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20749
21295
  };
20750
21296
 
20751
21297
  const getTextContent = async (pageIndex) => {
20752
- if (currentType === "text") {
20753
- const text = textPages[pageIndex] || "";
20754
- return [
20755
- {
20756
- str: text,
20757
- dir: "ltr",
20758
- width: 0,
20759
- height: 0,
20760
- transform: [1, 0, 0, 1, 0, 0],
20761
- fontName: "default",
20762
- },
20763
- ];
21298
+ if (currentType === 'text') {
21299
+ const text = textPages[pageIndex] || '';
21300
+ return [{
21301
+ str: text,
21302
+ dir: 'ltr',
21303
+ width: 0,
21304
+ height: 0,
21305
+ transform: [1, 0, 0, 1, 0, 0],
21306
+ fontName: 'default',
21307
+ }];
20764
21308
  }
20765
21309
 
20766
- if (currentType === "epub") {
21310
+ if (currentType === 'epub') {
20767
21311
  if (!book) return [];
20768
21312
  const item = spineItems[pageIndex];
20769
21313
  if (!item) return [];
20770
21314
  try {
20771
21315
  const section = book.spine.get(item.idref || item.href);
20772
- const text =
20773
- section && typeof section.text === "function"
20774
- ? await section.text()
20775
- : "";
21316
+ const text = section && typeof section.text === 'function' ? await section.text() : '';
20776
21317
  if (!text) return [];
20777
- return [
20778
- {
20779
- str: text,
20780
- dir: "ltr",
20781
- width: 0,
20782
- height: 0,
20783
- transform: [1, 0, 0, 1, 0, 0],
20784
- fontName: "default",
20785
- },
20786
- ];
21318
+ return [{
21319
+ str: text,
21320
+ dir: 'ltr',
21321
+ width: 0,
21322
+ height: 0,
21323
+ transform: [1, 0, 0, 1, 0, 0],
21324
+ fontName: 'default',
21325
+ }];
20787
21326
  } catch (err) {
20788
21327
  return [];
20789
21328
  }
@@ -20794,7 +21333,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20794
21333
 
20795
21334
  const getPageText = async (pageIndex) => {
20796
21335
  const items = await getTextContent(pageIndex);
20797
- return items.map((item) => item.str).join(" ");
21336
+ return items.map((item) => item.str).join(' ');
20798
21337
  };
20799
21338
 
20800
21339
  const searchText = async (query) => {
@@ -20810,11 +21349,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20810
21349
  while (pos !== -1) {
20811
21350
  const start = Math.max(0, pos - 20);
20812
21351
  const end = Math.min(text.length, pos + query.length + 20);
20813
- results.push({
20814
- pageIndex: i,
20815
- text: text.substring(start, end),
20816
- matchIndex,
20817
- });
21352
+ results.push({ pageIndex: i, text: text.substring(start, end), matchIndex });
20818
21353
  matchIndex += 1;
20819
21354
  pos = lower.indexOf(normalized, pos + 1);
20820
21355
  }
@@ -20830,11 +21365,11 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20830
21365
 
20831
21366
  const getPageIndex = (dest) => {
20832
21367
  if (!dest) return null;
20833
- if (typeof dest === "string") return getSpineIndexByHref(dest);
20834
- if (typeof dest !== "object") return null;
20835
- if (dest.kind === "href") return getSpineIndexByHref(dest.value);
20836
- if (dest.kind === "pageIndex") return dest.value;
20837
- if (dest.kind === "pageNumber") return Math.max(0, dest.value - 1);
21368
+ if (typeof dest === 'string') return getSpineIndexByHref(dest);
21369
+ if (typeof dest !== 'object') return null;
21370
+ if (dest.kind === 'href') return getSpineIndexByHref(dest.value);
21371
+ if (dest.kind === 'pageIndex') return dest.value;
21372
+ if (dest.kind === 'pageNumber') return Math.max(0, dest.value - 1);
20838
21373
  return null;
20839
21374
  };
20840
21375
 
@@ -20848,53 +21383,70 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20848
21383
  };
20849
21384
 
20850
21385
  const applyZoom = () => {
20851
- if (currentType === "text") {
21386
+ if (currentType === 'text') {
20852
21387
  applyTextZoom();
20853
- } else if (currentType === "epub") {
21388
+ } else if (currentType === 'epub') {
20854
21389
  applyEpubZoom();
21390
+ } else if (currentType === 'comic') {
21391
+ applyComicLayout();
20855
21392
  }
20856
21393
  };
20857
21394
 
20858
21395
  const handleCommand = async (message) => {
20859
21396
  const { id, kind, payload } = message;
20860
- log("command", kind);
20861
21397
 
20862
21398
  try {
20863
- if (kind === "load") {
21399
+ if (kind === 'load') {
20864
21400
  currentType = payload.type;
20865
21401
  zoom = 1.0;
20866
21402
 
20867
- if (currentType === "text") {
21403
+ if (currentType === 'text') {
20868
21404
  const result = await loadText(payload.source);
20869
21405
  sendState();
20870
21406
  sendResponse(id, true, result);
20871
21407
  return;
20872
21408
  }
20873
21409
 
20874
- if (currentType === "epub") {
21410
+ if (currentType === 'epub') {
20875
21411
  const result = await loadEpub(payload.source);
20876
21412
  sendState({ outline: result.outline || [] });
20877
21413
  sendResponse(id, true, result);
20878
21414
  return;
20879
21415
  }
20880
21416
 
20881
- throw new Error("Unsupported document type");
21417
+ if (currentType === 'comic') {
21418
+ const result = await loadComic(payload.source, payload.format || 'cbz');
21419
+ sendState();
21420
+ sendResponse(id, true, result);
21421
+ return;
21422
+ }
21423
+
21424
+ throw new Error('Unsupported document type');
20882
21425
  }
20883
21426
 
20884
- if (kind === "go-to-page") {
21427
+ if (kind === 'go-to-page') {
20885
21428
  const page = Math.max(1, payload.page || 1);
20886
- if (currentType === "text") {
21429
+ if (currentType === 'text') {
20887
21430
  currentPage = page;
20888
21431
  renderTextPage(page - 1);
20889
21432
  sendState();
20890
- } else if (currentType === "epub") {
21433
+ } else if (currentType === 'epub') {
20891
21434
  await displayEpubPage(page - 1);
21435
+ } else if (currentType === 'comic') {
21436
+ currentPage = page;
21437
+ const target = comicPages[page - 1];
21438
+ if (target) {
21439
+ await ensureComicPage(page - 1);
21440
+ target.scrollIntoView({ block: 'start' });
21441
+ renderComicPage(page - 1);
21442
+ sendState();
21443
+ }
20892
21444
  }
20893
21445
  sendResponse(id, true, { currentPage });
20894
21446
  return;
20895
21447
  }
20896
21448
 
20897
- if (kind === "set-zoom") {
21449
+ if (kind === 'set-zoom') {
20898
21450
  zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
20899
21451
  applyZoom();
20900
21452
  sendState();
@@ -20902,103 +21454,132 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20902
21454
  return;
20903
21455
  }
20904
21456
 
20905
- if (kind === "set-rotation") {
21457
+ if (kind === 'set-rotation') {
21458
+ rotation = ((payload.rotation || 0) % 360 + 360) % 360;
21459
+ if (currentType === 'comic') applyComicLayout();
20906
21460
  sendResponse(id, true, {});
20907
21461
  return;
20908
21462
  }
20909
21463
 
20910
- if (kind === "get-text-content") {
21464
+ if (kind === 'get-text-content') {
20911
21465
  const items = await getTextContent(payload.pageIndex || 0);
20912
21466
  sendResponse(id, true, items);
20913
21467
  return;
20914
21468
  }
20915
21469
 
20916
- if (kind === "get-page-dimensions") {
20917
- sendResponse(id, true, getPageDimensions());
21470
+ if (kind === 'get-page-dimensions') {
21471
+ if (currentType === 'comic') {
21472
+ sendResponse(
21473
+ id,
21474
+ true,
21475
+ await getComicPageDimensions(payload.pageIndex || 0)
21476
+ );
21477
+ } else {
21478
+ sendResponse(id, true, getPageDimensions());
21479
+ }
20918
21480
  return;
20919
21481
  }
20920
21482
 
20921
- if (kind === "search-text") {
20922
- const results = await searchText(payload.query || "");
21483
+ if (kind === 'get-page-preview') {
21484
+ if (currentType !== 'comic') {
21485
+ sendResponse(id, true, null);
21486
+ return;
21487
+ }
21488
+ sendResponse(
21489
+ id,
21490
+ true,
21491
+ await getComicPagePreview(payload.pageIndex || 0)
21492
+ );
21493
+ return;
21494
+ }
21495
+
21496
+ if (kind === 'search-text') {
21497
+ const results = await searchText(payload.query || '');
20923
21498
  sendResponse(id, true, results);
20924
21499
  return;
20925
21500
  }
20926
21501
 
20927
- if (kind === "select-text") {
21502
+ if (kind === 'select-text') {
20928
21503
  const selection = await selectText(payload.pageIndex || 0);
20929
21504
  sendResponse(id, true, selection);
20930
21505
  return;
20931
21506
  }
20932
21507
 
20933
- if (kind === "get-outline") {
21508
+ if (kind === 'get-outline') {
20934
21509
  const outline = await buildOutline();
20935
21510
  sendResponse(id, true, outline);
20936
21511
  return;
20937
21512
  }
20938
21513
 
20939
- if (kind === "get-page-index") {
21514
+ if (kind === 'get-page-index') {
20940
21515
  const index = getPageIndex(payload.dest);
20941
21516
  sendResponse(id, true, index);
20942
21517
  return;
20943
21518
  }
20944
21519
 
20945
- if (kind === "destroy") {
21520
+ if (kind === 'destroy') {
20946
21521
  clearViewer();
20947
21522
  sendResponse(id, true, {});
20948
21523
  return;
20949
21524
  }
20950
21525
 
20951
- sendResponse(id, false, null, "Unknown command");
21526
+ sendResponse(id, false, null, 'Unknown command');
20952
21527
  } catch (err) {
20953
- sendResponse(
20954
- id,
20955
- false,
20956
- null,
20957
- err && err.message ? err.message : String(err)
20958
- );
21528
+ sendResponse(id, false, null, err && err.message ? err.message : String(err));
20959
21529
  }
20960
21530
  };
20961
21531
 
20962
21532
  const onMessage = (event) => {
20963
21533
  let message = null;
20964
- const raw = event && typeof event === "object" ? event.data : null;
21534
+ const raw = event && typeof event === 'object' ? event.data : null;
20965
21535
 
20966
- if (raw && typeof raw === "object" && raw.kind && raw.id) {
21536
+ if (raw && typeof raw === 'object' && (raw.kind || raw.type) && raw.id) {
20967
21537
  message = raw;
20968
- } else if (typeof raw === "string") {
20969
- if (raw.startsWith("setImmediate$")) {
20970
- return;
20971
- }
21538
+ } else if (typeof raw === 'string') {
21539
+ if (raw.startsWith('setImmediate$')) return;
20972
21540
  const trimmed = raw.trim();
20973
- if (
20974
- !trimmed ||
20975
- (!trimmed.startsWith("{") && !trimmed.startsWith("["))
20976
- ) {
21541
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
20977
21542
  return;
20978
21543
  }
20979
21544
  try {
20980
21545
  message = JSON.parse(trimmed);
20981
21546
  } catch (err) {
20982
- sendEvent("RUNTIME_ERROR", {
20983
- message: "Failed to parse message",
20984
- context: "runtime.onMessage",
20985
- stack: err && err.stack ? err.stack : null,
20986
- preview: trimmed.slice(0, 200),
20987
- });
20988
21547
  return;
20989
21548
  }
20990
21549
  } else {
20991
21550
  return;
20992
21551
  }
20993
21552
 
20994
- if (!message || !message.kind || message.id == null) return;
20995
- handleCommand(message);
21553
+ if (!message || message.id == null) return;
21554
+
21555
+ if (message.type === 'asset-response') {
21556
+ const pending = runtimeAssetRequests.get(message.id);
21557
+ if (!pending) return;
21558
+ runtimeAssetRequests.delete(message.id);
21559
+ clearTimeout(pending.timeout);
21560
+ if (message.ok) pending.resolve(message.data);
21561
+ else pending.reject(new Error(message.error || 'Falha ao carregar asset CBR.'));
21562
+ return;
21563
+ }
21564
+
21565
+ if (message.type === 'file-chunk-response') {
21566
+ const pending = fileChunkRequests.get(message.id);
21567
+ if (!pending) return;
21568
+ fileChunkRequests.delete(message.id);
21569
+ clearTimeout(pending.timeout);
21570
+ if (message.ok) pending.resolve(message);
21571
+ else pending.reject(new Error(message.error || 'Falha ao ler arquivo local.'));
21572
+ return;
21573
+ }
21574
+
21575
+ if (!message.kind) return;
21576
+ handleCommand(message);
20996
21577
  };
20997
21578
 
20998
- window.addEventListener("message", onMessage);
20999
- document.addEventListener("message", onMessage);
21579
+ window.addEventListener('message', onMessage);
21580
+ document.addEventListener('message', onMessage);
21000
21581
 
21001
- sendMessage({ type: "ready" });
21582
+ sendMessage({ type: 'ready' });
21002
21583
  })();
21003
21584
  </script>
21004
21585
  </body>
@@ -21013,7 +21594,7 @@ import {
21013
21594
  useRef as useRef4,
21014
21595
  useEffect as useEffect4,
21015
21596
  useCallback as useCallback3,
21016
- useState as useState3
21597
+ useState as useState4
21017
21598
  } from "react";
21018
21599
  import {
21019
21600
  FlatList,
@@ -21310,6 +21891,12 @@ var buildCommentTapGestureDeps = ({
21310
21891
  addAnnotationAt
21311
21892
  ];
21312
21893
 
21894
+ // components/pdfPageMetrics.ts
21895
+ var resolvePdfBasePageWidth = ({
21896
+ viewportWidth,
21897
+ horizontalPadding
21898
+ }) => Math.max(0, viewportWidth - horizontalPadding * 2);
21899
+
21313
21900
  // components/PageRenderer.tsx
21314
21901
  import { jsx, jsxs } from "react/jsx-runtime";
21315
21902
  var TEXT_MARKUP_TOOLS = /* @__PURE__ */ new Set([
@@ -22271,7 +22858,10 @@ var PageRenderer = ({
22271
22858
  }, [pageTheme]);
22272
22859
  const aspectRatio = typeof pageAspectRatio === "number" && Number.isFinite(pageAspectRatio) && pageAspectRatio > 0 ? pageAspectRatio : pageSize && pageSize.width > 0 && pageSize.height > 0 ? pageSize.width / pageSize.height : 0.77;
22273
22860
  const containerWidth = availableWidth ?? windowWidth;
22274
- const baseWidth = containerWidth * 0.92;
22861
+ const baseWidth = resolvePdfBasePageWidth({
22862
+ viewportWidth: containerWidth,
22863
+ horizontalPadding
22864
+ });
22275
22865
  const pageWidth = isNative ? baseWidth * zoom : baseWidth;
22276
22866
  const pageHeight = pageWidth / aspectRatio;
22277
22867
  const pageFrameWidth = Math.max(
@@ -22995,33 +23585,104 @@ var arePageRendererPropsEqual = (previous, next) => previous.engine === next.eng
22995
23585
  var PageRenderer_default = memo(PageRenderer, arePageRendererPropsEqual);
22996
23586
 
22997
23587
  // components/WebViewViewer.tsx
22998
- import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
23588
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
22999
23589
  import { Image, StyleSheet as StyleSheet2, View as View2 } from "react-native";
23000
23590
  import WebView from "react-native-webview";
23001
23591
  import { useViewerStore as useViewerStore2 } from "@papyrus-sdk/core";
23592
+
23593
+ // components/webViewState.ts
23594
+ var parseWebViewState = (raw) => {
23595
+ try {
23596
+ const message = JSON.parse(raw);
23597
+ if (!message || typeof message !== "object") return null;
23598
+ const typedMessage = message;
23599
+ if (typedMessage.type !== "state") return null;
23600
+ if (!typedMessage.payload || typeof typedMessage.payload !== "object") {
23601
+ return null;
23602
+ }
23603
+ const payload = typedMessage.payload;
23604
+ const update = {};
23605
+ if (typeof payload.currentPage === "number" && Number.isInteger(payload.currentPage) && payload.currentPage >= 1) {
23606
+ update.currentPage = payload.currentPage;
23607
+ }
23608
+ if (typeof payload.pageCount === "number" && Number.isInteger(payload.pageCount) && payload.pageCount >= 0) {
23609
+ update.pageCount = payload.pageCount;
23610
+ }
23611
+ return Object.keys(update).length > 0 ? update : null;
23612
+ } catch {
23613
+ return null;
23614
+ }
23615
+ };
23616
+
23617
+ // components/WebViewViewer.tsx
23002
23618
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
23003
23619
  var runtimeAsset = require_runtime();
23004
- var runtimeSource = (() => {
23005
- if (typeof runtimeAsset === "string") {
23006
- return { html: runtimeAsset };
23620
+ var resolveRuntimeSource = (asset) => {
23621
+ if (typeof asset === "string") {
23622
+ return { html: asset };
23007
23623
  }
23008
- if (typeof runtimeAsset === "number") {
23009
- const resolved = Image.resolveAssetSource(runtimeAsset);
23624
+ if (typeof asset === "number") {
23625
+ const resolved = Image.resolveAssetSource(asset);
23010
23626
  if (resolved?.uri) {
23011
23627
  return { uri: resolved.uri };
23012
23628
  }
23013
23629
  }
23014
- if (runtimeAsset && typeof runtimeAsset === "object" && "uri" in runtimeAsset) {
23015
- const uri = runtimeAsset.uri;
23630
+ if (asset && typeof asset === "object" && "uri" in asset) {
23631
+ const uri = asset.uri;
23016
23632
  if (uri) return { uri };
23017
23633
  }
23018
23634
  return { html: "" };
23019
- })();
23635
+ };
23020
23636
  var WebViewViewer = ({ engine }) => {
23021
23637
  const webViewRef = useRef2(null);
23022
23638
  const { pageTheme } = useViewerStore2();
23639
+ const bridgeEngine = engine;
23640
+ const runtimeSource = useMemo2(
23641
+ () => resolveRuntimeSource(
23642
+ bridgeEngine.getWebViewRuntimeSource?.() ?? runtimeAsset
23643
+ ),
23644
+ [bridgeEngine]
23645
+ );
23646
+ const [runtimeHtml, setRuntimeHtml] = useState2(null);
23647
+ useEffect2(() => {
23648
+ let active = true;
23649
+ if ("html" in runtimeSource) {
23650
+ setRuntimeHtml(runtimeSource.html);
23651
+ return () => {
23652
+ active = false;
23653
+ };
23654
+ }
23655
+ setRuntimeHtml(null);
23656
+ void fetch(runtimeSource.uri).then((response) => {
23657
+ if (!response.ok) throw new Error(`status ${response.status}`);
23658
+ return response.text();
23659
+ }).then((html) => {
23660
+ if (active) setRuntimeHtml(html);
23661
+ }).catch(() => {
23662
+ if (active) setRuntimeHtml(null);
23663
+ });
23664
+ return () => {
23665
+ active = false;
23666
+ };
23667
+ }, [runtimeSource]);
23668
+ const webViewSource = useMemo2(() => {
23669
+ if (runtimeHtml !== null) {
23670
+ return {
23671
+ html: runtimeHtml,
23672
+ baseUrl: "http://localhost:3005/"
23673
+ };
23674
+ }
23675
+ if ("uri" in runtimeSource) return { html: "" };
23676
+ return runtimeSource;
23677
+ }, [runtimeHtml, runtimeSource]);
23678
+ const runtimeConfig = bridgeEngine.getWebViewRuntimeConfig?.();
23679
+ const runtimeConfigScript = useMemo2(() => {
23680
+ if (!runtimeConfig) return void 0;
23681
+ return `window.__PAPYRUS_RUNTIME_CONFIG__=${JSON.stringify(
23682
+ runtimeConfig
23683
+ )};true;`;
23684
+ }, [runtimeConfig]);
23023
23685
  useEffect2(() => {
23024
- const bridgeEngine = engine;
23025
23686
  bridgeEngine.attachWebView?.({
23026
23687
  postMessage: (message) => {
23027
23688
  if (__DEV__) {
@@ -23031,9 +23692,22 @@ var WebViewViewer = ({ engine }) => {
23031
23692
  webViewRef.current?.postMessage(message);
23032
23693
  }
23033
23694
  });
23034
- }, [engine]);
23695
+ }, [bridgeEngine]);
23035
23696
  const handleMessage = (event) => {
23036
- const bridgeEngine = engine;
23697
+ const state = parseWebViewState(event.nativeEvent.data);
23698
+ if (state) {
23699
+ const viewerState = useViewerStore2.getState();
23700
+ const nextState = {};
23701
+ if (state.currentPage !== void 0 && state.currentPage !== viewerState.currentPage) {
23702
+ nextState.currentPage = state.currentPage;
23703
+ }
23704
+ if (state.pageCount !== void 0 && state.pageCount !== viewerState.pageCount) {
23705
+ nextState.pageCount = state.pageCount;
23706
+ }
23707
+ if (Object.keys(nextState).length > 0) {
23708
+ viewerState.setDocumentState(nextState);
23709
+ }
23710
+ }
23037
23711
  if (__DEV__) {
23038
23712
  console.log("[Papyrus WebView] message", event.nativeEvent.data);
23039
23713
  }
@@ -23073,11 +23747,12 @@ var WebViewViewer = ({ engine }) => {
23073
23747
  WebView,
23074
23748
  {
23075
23749
  ref: webViewRef,
23076
- source: runtimeSource,
23750
+ source: webViewSource,
23077
23751
  originWhitelist: ["*"],
23078
23752
  onMessage: handleMessage,
23079
23753
  onLoadEnd: handleLoadEnd,
23080
23754
  onError: handleError,
23755
+ injectedJavaScriptBeforeContentLoaded: runtimeConfigScript,
23081
23756
  javaScriptEnabled: true,
23082
23757
  domStorageEnabled: true,
23083
23758
  scalesPageToFit: true,
@@ -23127,7 +23802,7 @@ var styles2 = StyleSheet2.create({
23127
23802
  var WebViewViewer_default = WebViewViewer;
23128
23803
 
23129
23804
  // components/DedicatedAndroidPdfViewer.tsx
23130
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
23805
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
23131
23806
  import { Pressable as Pressable2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
23132
23807
  import { useViewerStore as useViewerStore3 } from "@papyrus-sdk/core";
23133
23808
  import { PapyrusPdfDocumentView } from "@papyrus-sdk/engine-native";
@@ -23773,7 +24448,7 @@ function DedicatedAndroidPdfViewer({
23773
24448
  const addAnnotation = useViewerStore3((state) => state.addAnnotation);
23774
24449
  const setSelectedAnnotation = useViewerStore3((state) => state.setSelectedAnnotation);
23775
24450
  const engineId = getDedicatedAndroidPdfEngineId(engine);
23776
- const [selection, setSelection] = useState2(null);
24451
+ const [selection, setSelection] = useState3(null);
23777
24452
  const selectionRef = useRef3(null);
23778
24453
  const lastVisiblePagesKeyRef = useRef3("");
23779
24454
  const lastScrollOffsetYRef = useRef3(0);
@@ -24126,13 +24801,13 @@ var Viewer = ({
24126
24801
  const lastScrollOffsetYRef = useRef4(0);
24127
24802
  const scrollDownAccumRef = useRef4(0);
24128
24803
  const scrollUpAccumRef = useRef4(0);
24129
- const [layoutRevision, setLayoutRevision] = useState3(0);
24130
- const [selectionDragActive, setSelectionDragActive] = useState3(false);
24804
+ const [layoutRevision, setLayoutRevision] = useState4(0);
24805
+ const [selectionDragActive, setSelectionDragActive] = useState4(false);
24131
24806
  const selectionDragActiveRef = useRef4(false);
24132
- const [gestureScrollLockActive, setGestureScrollLockActive] = useState3(false);
24807
+ const [gestureScrollLockActive, setGestureScrollLockActive] = useState4(false);
24133
24808
  const gestureScrollLockActiveRef = useRef4(false);
24134
- const [pinchPreviewScale, setPinchPreviewScale] = useState3(1);
24135
- const [lastPinchEndedAt, setLastPinchEndedAt] = useState3(null);
24809
+ const [pinchPreviewScale, setPinchPreviewScale] = useState4(1);
24810
+ const [lastPinchEndedAt, setLastPinchEndedAt] = useState4(null);
24136
24811
  const pinchGestureActiveRef = useRef4(false);
24137
24812
  const pinchStartZoomRef = useRef4(1);
24138
24813
  const pinchPreviewZoomRef = useRef4(1);
@@ -24418,7 +25093,10 @@ var Viewer = ({
24418
25093
  const getPageWidthForZoom = useCallback3(
24419
25094
  (pageIndex, zoomValue) => {
24420
25095
  const safeZoom = Math.max(zoomValue, 0.25);
24421
- const baseWidth = isDouble ? columnWidth * 0.92 : windowWidth * 0.92;
25096
+ const baseWidth = resolvePdfBasePageWidth({
25097
+ viewportWidth: isDouble ? columnWidth : windowWidth,
25098
+ horizontalPadding: isDouble ? 8 : 16
25099
+ });
24422
25100
  return baseWidth * safeZoom;
24423
25101
  },
24424
25102
  [columnWidth, isDouble, windowWidth]
@@ -24951,7 +25629,10 @@ var Viewer = ({
24951
25629
  let offset = LIST_TOP_PADDING;
24952
25630
  if (isDouble) {
24953
25631
  const safeZoom2 = Math.max(zoom, 0.25);
24954
- const pageWidth2 = columnWidth * 0.92 * safeZoom2;
25632
+ const pageWidth2 = resolvePdfBasePageWidth({
25633
+ viewportWidth: columnWidth,
25634
+ horizontalPadding: 8
25635
+ }) * safeZoom2;
24955
25636
  const estimatedLength2 = pageWidth2 / DEFAULT_PAGE_ASPECT_RATIO + DOUBLE_PAGE_SPACING;
24956
25637
  for (let i = 0; i < rows.length; i += 1) {
24957
25638
  const row = rows[i];
@@ -24967,7 +25648,10 @@ var Viewer = ({
24967
25648
  return { offsets, lengths, estimatedLength: estimatedLength2 };
24968
25649
  }
24969
25650
  const safeZoom = Math.max(zoom, 0.25);
24970
- const pageWidth = windowWidth * 0.92 * safeZoom;
25651
+ const pageWidth = resolvePdfBasePageWidth({
25652
+ viewportWidth: windowWidth,
25653
+ horizontalPadding: 16
25654
+ }) * safeZoom;
24971
25655
  const estimatedLength = pageWidth / DEFAULT_PAGE_ASPECT_RATIO + CONTINUOUS_PAGE_SPACING;
24972
25656
  for (let i = 0; i < pageCount; i += 1) {
24973
25657
  const ratio = getPageAspectRatio(i);
@@ -25456,7 +26140,7 @@ var styles4 = StyleSheet4.create({
25456
26140
  var Viewer_default = Viewer;
25457
26141
 
25458
26142
  // components/Topbar.tsx
25459
- import { useState as useState5 } from "react";
26143
+ import { useState as useState6 } from "react";
25460
26144
  import { View as View6, Text as Text2, Pressable as Pressable4, StyleSheet as StyleSheet6, useWindowDimensions as useWindowDimensions3 } from "react-native";
25461
26145
  import { useViewerStore as useViewerStore5 } from "@papyrus-sdk/core";
25462
26146
 
@@ -25479,7 +26163,7 @@ var MOBILE_CHROME_METRICS = {
25479
26163
  };
25480
26164
 
25481
26165
  // components/PageJumpModal.tsx
25482
- import { useEffect as useEffect5, useState as useState4 } from "react";
26166
+ import { useEffect as useEffect5, useState as useState5 } from "react";
25483
26167
  import { Modal, Pressable as Pressable3, StyleSheet as StyleSheet5, Text, TextInput, View as View5 } from "react-native";
25484
26168
 
25485
26169
  // components/pageJumpModel.ts
@@ -25501,7 +26185,7 @@ function PageJumpModal({
25501
26185
  onClose,
25502
26186
  onConfirm
25503
26187
  }) {
25504
- const [value, setValue] = useState4(`${currentPage}`);
26188
+ const [value, setValue] = useState5(`${currentPage}`);
25505
26189
  useEffect5(() => {
25506
26190
  if (visible) setValue(`${currentPage}`);
25507
26191
  }, [currentPage, visible]);
@@ -25662,7 +26346,7 @@ var Topbar = ({
25662
26346
  accentColor,
25663
26347
  mobileChromeVisible
25664
26348
  } = useViewerStore5();
25665
- const [jumpModalOpen, setJumpModalOpen] = useState5(false);
26349
+ const [jumpModalOpen, setJumpModalOpen] = useState6(false);
25666
26350
  const isDark = uiTheme === "dark";
25667
26351
  const navIconColor = isDark ? "#e5e7eb" : "#111827";
25668
26352
  const { width, height } = useWindowDimensions3();
@@ -25965,7 +26649,7 @@ var styles6 = StyleSheet6.create({
25965
26649
  var Topbar_default = Topbar;
25966
26650
 
25967
26651
  // components/ToolDock.tsx
25968
- import React7, { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef5, useState as useState6 } from "react";
26652
+ import React7, { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef5, useState as useState7 } from "react";
25969
26653
  import {
25970
26654
  View as View7,
25971
26655
  Text as Text3,
@@ -26203,8 +26887,8 @@ var OpacitySlider = React7.memo(function OpacitySlider2({
26203
26887
  isDark,
26204
26888
  onCommit
26205
26889
  }) {
26206
- const [trackWidth, setTrackWidth] = useState6(0);
26207
- const [draftValue, setDraftValue] = useState6(value);
26890
+ const [trackWidth, setTrackWidth] = useState7(0);
26891
+ const [draftValue, setDraftValue] = useState7(value);
26208
26892
  const dragValueRef = useRef5(draftValue);
26209
26893
  const isDraggingRef = useRef5(false);
26210
26894
  const frameRef = useRef5(null);
@@ -26313,8 +26997,8 @@ var ToolDock = () => {
26313
26997
  undoAnnotations,
26314
26998
  redoAnnotations
26315
26999
  } = useViewerStore6();
26316
- const [paletteExpanded, setPaletteExpanded] = useState6(false);
26317
- const [extrasExpanded, setExtrasExpanded] = useState6(false);
27000
+ const [paletteExpanded, setPaletteExpanded] = useState7(false);
27001
+ const [extrasExpanded, setExtrasExpanded] = useState7(false);
26318
27002
  const isDark = uiTheme === "dark";
26319
27003
  const { width: windowWidth } = useWindowDimensions4();
26320
27004
  const t = getStrings(locale);
@@ -27037,9 +27721,10 @@ var withInkAlpha = (hex, alpha) => {
27037
27721
  var ToolDock_default = ToolDock;
27038
27722
 
27039
27723
  // components/RightSheet.tsx
27040
- import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef6, useState as useState7 } from "react";
27724
+ import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef6, useState as useState8 } from "react";
27041
27725
  import {
27042
27726
  Dimensions,
27727
+ Image as Image2,
27043
27728
  Platform as Platform3,
27044
27729
  Pressable as Pressable7,
27045
27730
  StyleSheet as StyleSheet9,
@@ -27230,11 +27915,13 @@ var PageThumbnail = ({
27230
27915
  frameHeight,
27231
27916
  accentColor,
27232
27917
  useNativePreview,
27918
+ useImagePreview,
27233
27919
  shouldRenderPreview,
27234
27920
  onPress
27235
27921
  }) => {
27236
27922
  const viewRef = useRef6(null);
27237
- const [layoutReady, setLayoutReady] = useState7(false);
27923
+ const [layoutReady, setLayoutReady] = useState8(false);
27924
+ const [previewUri, setPreviewUri] = useState8(null);
27238
27925
  useEffect7(() => {
27239
27926
  if (!layoutReady || !useNativePreview || !shouldRenderPreview) return;
27240
27927
  const viewTag = findNodeHandle2(viewRef.current);
@@ -27250,6 +27937,24 @@ var PageThumbnail = ({
27250
27937
  useNativePreview,
27251
27938
  zoom
27252
27939
  ]);
27940
+ useEffect7(() => {
27941
+ if (!useImagePreview || !shouldRenderPreview) {
27942
+ setPreviewUri(null);
27943
+ return;
27944
+ }
27945
+ let active = true;
27946
+ const previewEngine = engine;
27947
+ const request = previewEngine.getPagePreview?.(pageIndex);
27948
+ if (!request) return;
27949
+ void request.then((uri) => {
27950
+ if (active) setPreviewUri(uri);
27951
+ }).catch(() => {
27952
+ if (active) setPreviewUri(null);
27953
+ });
27954
+ return () => {
27955
+ active = false;
27956
+ };
27957
+ }, [engine, pageIndex, shouldRenderPreview, useImagePreview]);
27253
27958
  const handleLayout = (event) => {
27254
27959
  if (event.nativeEvent.layout.width && event.nativeEvent.layout.height) {
27255
27960
  setLayoutReady(true);
@@ -27272,7 +27977,14 @@ var PageThumbnail = ({
27272
27977
  {
27273
27978
  onLayout: handleLayout,
27274
27979
  style: [styles9.thumbFrame, { width: frameWidth, height: frameHeight }],
27275
- children: useNativePreview && shouldRenderPreview ? /* @__PURE__ */ jsx10(PapyrusPageView2, { ref: viewRef, style: styles9.thumbView }) : /* @__PURE__ */ jsx10(View9, { style: [styles9.thumbFallback, isDark && styles9.thumbFallbackDark], children: /* @__PURE__ */ jsx10(
27980
+ children: useNativePreview && shouldRenderPreview ? /* @__PURE__ */ jsx10(PapyrusPageView2, { ref: viewRef, style: styles9.thumbView }) : useImagePreview && shouldRenderPreview && previewUri ? /* @__PURE__ */ jsx10(
27981
+ Image2,
27982
+ {
27983
+ source: { uri: previewUri },
27984
+ resizeMode: "contain",
27985
+ style: styles9.thumbImage
27986
+ }
27987
+ ) : /* @__PURE__ */ jsx10(View9, { style: [styles9.thumbFallback, isDark && styles9.thumbFallbackDark], children: /* @__PURE__ */ jsx10(
27276
27988
  Text5,
27277
27989
  {
27278
27990
  style: [
@@ -27349,9 +28061,14 @@ var RightSheet = ({
27349
28061
  locale,
27350
28062
  accentColor
27351
28063
  } = useViewerStore7();
27352
- const [pagesMode, setPagesMode] = useState7(
27353
- documentType === "pdf" ? "thumbnails" : "summary"
28064
+ const [pagesMode, setPagesMode] = useState8(
28065
+ documentType === "pdf" || documentType === "comic" ? "thumbnails" : "summary"
27354
28066
  );
28067
+ useEffect7(() => {
28068
+ setPagesMode(
28069
+ documentType === "pdf" || documentType === "comic" ? "thumbnails" : "summary"
28070
+ );
28071
+ }, [documentType]);
27355
28072
  const isDark = uiTheme === "dark";
27356
28073
  const t = getStrings(locale);
27357
28074
  const showingNotes = sidebarRightTab === "annotations";
@@ -27369,11 +28086,12 @@ var RightSheet = ({
27369
28086
  UIManager.getViewManagerConfig?.("PapyrusPageView")
27370
28087
  );
27371
28088
  const useNativePreview = renderTarget !== "webview" && hasNativePageView;
28089
+ const useImagePreview = documentType === "comic" && renderTarget === "webview" && typeof engine.getPagePreview === "function";
27372
28090
  const thumbnailDimensionsCacheRef = useRef6(/* @__PURE__ */ new Map());
27373
28091
  const thumbnailDimensionsPendingRef = useRef6(/* @__PURE__ */ new Set());
27374
28092
  const thumbnailRefreshTimeoutRef = useRef6(null);
27375
- const [thumbnailLayoutRevision, setThumbnailLayoutRevision] = useState7(0);
27376
- const [visibleThumbnailPages, setVisibleThumbnailPages] = useState7(
28093
+ const [thumbnailLayoutRevision, setThumbnailLayoutRevision] = useState8(0);
28094
+ const [visibleThumbnailPages, setVisibleThumbnailPages] = useState8(
27377
28095
  () => /* @__PURE__ */ new Set()
27378
28096
  );
27379
28097
  const resolvedThumbsInitialCount = useMemo5(
@@ -27525,7 +28243,7 @@ var RightSheet = ({
27525
28243
  );
27526
28244
  const renderThumbnailItem = useCallback4(
27527
28245
  ({ item }) => {
27528
- const shouldRenderPreview = useNativePreview && (visibleThumbnailPages.has(item) || item < resolvedThumbsPrewarmCount || Math.abs(item + 1 - currentPage) <= 1);
28246
+ const shouldRenderPreview = (useNativePreview || useImagePreview) && (visibleThumbnailPages.has(item) || item < resolvedThumbsPrewarmCount || Math.abs(item + 1 - currentPage) <= 1);
27529
28247
  return /* @__PURE__ */ jsx10(
27530
28248
  PageThumbnail,
27531
28249
  {
@@ -27539,6 +28257,7 @@ var RightSheet = ({
27539
28257
  frameHeight: getThumbnailFrameHeight(item),
27540
28258
  accentColor,
27541
28259
  useNativePreview,
28260
+ useImagePreview,
27542
28261
  shouldRenderPreview,
27543
28262
  onPress: () => jumpToPage(item)
27544
28263
  }
@@ -27555,6 +28274,7 @@ var RightSheet = ({
27555
28274
  jumpToPage,
27556
28275
  resolvedThumbsPrewarmCount,
27557
28276
  useNativePreview,
28277
+ useImagePreview,
27558
28278
  visibleThumbnailPages,
27559
28279
  zoom
27560
28280
  ]
@@ -27886,6 +28606,10 @@ var styles9 = StyleSheet9.create({
27886
28606
  width: "100%",
27887
28607
  height: "100%"
27888
28608
  },
28609
+ thumbImage: {
28610
+ width: "100%",
28611
+ height: "100%"
28612
+ },
27889
28613
  thumbFallback: {
27890
28614
  flex: 1,
27891
28615
  alignItems: "center",
@@ -28026,7 +28750,7 @@ var styles9 = StyleSheet9.create({
28026
28750
  var RightSheet_default = RightSheet;
28027
28751
 
28028
28752
  // components/AnnotationEditor.tsx
28029
- import { useEffect as useEffect8, useState as useState8 } from "react";
28753
+ import { useEffect as useEffect8, useState as useState9 } from "react";
28030
28754
  import {
28031
28755
  Modal as Modal3,
28032
28756
  View as View10,
@@ -28049,7 +28773,7 @@ var AnnotationEditor = () => {
28049
28773
  } = useViewerStore8();
28050
28774
  const annotation = annotations.find((ann) => ann.id === selectedAnnotationId);
28051
28775
  const isEditable = annotation && (annotation.type === "text" || annotation.type === "comment");
28052
- const [draft, setDraft] = useState8("");
28776
+ const [draft, setDraft] = useState9("");
28053
28777
  const isDark = uiTheme === "dark";
28054
28778
  const t = getStrings(locale);
28055
28779
  useEffect8(() => {
@@ -29032,7 +29756,7 @@ var styles12 = StyleSheet12.create({
29032
29756
  var SettingsSheet_default = SettingsSheet;
29033
29757
 
29034
29758
  // components/CoverPreview.tsx
29035
- import { useEffect as useEffect9, useMemo as useMemo6, useRef as useRef7, useState as useState9 } from "react";
29759
+ import { useEffect as useEffect9, useMemo as useMemo6, useRef as useRef7, useState as useState10 } from "react";
29036
29760
  import {
29037
29761
  ActivityIndicator,
29038
29762
  StyleSheet as StyleSheet13,
@@ -29090,11 +29814,11 @@ var CoverPreview = ({
29090
29814
  onLoadEnd,
29091
29815
  onError
29092
29816
  }) => {
29093
- const [engine, setEngine] = useState9(null);
29094
- const [layoutReady, setLayoutReady] = useState9(false);
29095
- const [loaded, setLoaded] = useState9(false);
29096
- const [loading, setLoading] = useState9(false);
29097
- const [error, setError] = useState9(null);
29817
+ const [engine, setEngine] = useState10(null);
29818
+ const [layoutReady, setLayoutReady] = useState10(false);
29819
+ const [loaded, setLoaded] = useState10(false);
29820
+ const [loading, setLoading] = useState10(false);
29821
+ const [error, setError] = useState10(null);
29098
29822
  const viewRef = useRef7(null);
29099
29823
  const resolvedType = useMemo6(() => inferDocumentType(source, type), [source, type]);
29100
29824
  const isPdf = resolvedType === "pdf";
@@ -29214,7 +29938,7 @@ var styles13 = StyleSheet13.create({
29214
29938
  var CoverPreview_default = CoverPreview;
29215
29939
 
29216
29940
  // components/ReadingShell.tsx
29217
- import React13, { useState as useState11 } from "react";
29941
+ import React13, { useState as useState12 } from "react";
29218
29942
  import { Keyboard as Keyboard2, View as View20, StyleSheet as StyleSheet20 } from "react-native";
29219
29943
  import { useViewerStore as useViewerStore17 } from "@papyrus-sdk/core";
29220
29944
 
@@ -29839,7 +30563,7 @@ var styles17 = StyleSheet17.create({
29839
30563
  });
29840
30564
 
29841
30565
  // components/SearchOverlay.tsx
29842
- import { useEffect as useEffect10, useMemo as useMemo8, useState as useState10 } from "react";
30566
+ import { useEffect as useEffect10, useMemo as useMemo8, useState as useState11 } from "react";
29843
30567
  import {
29844
30568
  Keyboard,
29845
30569
  Pressable as Pressable15,
@@ -29868,8 +30592,8 @@ function SearchOverlay({
29868
30592
  uiTheme,
29869
30593
  accentColor
29870
30594
  } = useViewerStore15();
29871
- const [draft, setDraft] = useState10(searchQuery);
29872
- const [isSearching, setIsSearching] = useState10(false);
30595
+ const [draft, setDraft] = useState11(searchQuery);
30596
+ const [isSearching, setIsSearching] = useState11(false);
29873
30597
  const isDark = uiTheme === "dark";
29874
30598
  const t = getStrings(locale);
29875
30599
  const searchService = useMemo8(() => new SearchService(engine), [engine]);
@@ -30360,12 +31084,12 @@ function ReadingShell({
30360
31084
  uiTheme,
30361
31085
  accentColor
30362
31086
  } = useViewerStore17();
30363
- const [overflowOpen, setOverflowOpen] = useState11(false);
30364
- const [infoOpen, setInfoOpen] = useState11(false);
30365
- const [actionsOpen, setActionsOpen] = useState11(false);
30366
- const [settingsOpen, setSettingsOpen] = useState11(false);
30367
- const [searchResultsOpen, setSearchResultsOpen] = useState11(false);
30368
- const [pageJumpOpen, setPageJumpOpen] = useState11(false);
31087
+ const [overflowOpen, setOverflowOpen] = useState12(false);
31088
+ const [infoOpen, setInfoOpen] = useState12(false);
31089
+ const [actionsOpen, setActionsOpen] = useState12(false);
31090
+ const [settingsOpen, setSettingsOpen] = useState12(false);
31091
+ const [searchResultsOpen, setSearchResultsOpen] = useState12(false);
31092
+ const [pageJumpOpen, setPageJumpOpen] = useState12(false);
30369
31093
  const isDark = uiTheme === "dark";
30370
31094
  React13.useEffect(() => {
30371
31095
  const showSubscription = Keyboard2.addListener("keyboardDidShow", () => {