@papyrus-sdk/ui-react-native 0.2.20 → 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);
20717
+ };
20718
+
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';
20520
20743
  };
20521
20744
 
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 + ")"
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" });
20667
- }
20668
- if (book && book.opened) {
20669
- await withTimeout(book.opened, EPUB_OPEN_TIMEOUT_MS, "epub.opened");
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;
20670
21241
  }
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,
@@ -23004,33 +23585,104 @@ var arePageRendererPropsEqual = (previous, next) => previous.engine === next.eng
23004
23585
  var PageRenderer_default = memo(PageRenderer, arePageRendererPropsEqual);
23005
23586
 
23006
23587
  // components/WebViewViewer.tsx
23007
- 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";
23008
23589
  import { Image, StyleSheet as StyleSheet2, View as View2 } from "react-native";
23009
23590
  import WebView from "react-native-webview";
23010
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
23011
23618
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
23012
23619
  var runtimeAsset = require_runtime();
23013
- var runtimeSource = (() => {
23014
- if (typeof runtimeAsset === "string") {
23015
- return { html: runtimeAsset };
23620
+ var resolveRuntimeSource = (asset) => {
23621
+ if (typeof asset === "string") {
23622
+ return { html: asset };
23016
23623
  }
23017
- if (typeof runtimeAsset === "number") {
23018
- const resolved = Image.resolveAssetSource(runtimeAsset);
23624
+ if (typeof asset === "number") {
23625
+ const resolved = Image.resolveAssetSource(asset);
23019
23626
  if (resolved?.uri) {
23020
23627
  return { uri: resolved.uri };
23021
23628
  }
23022
23629
  }
23023
- if (runtimeAsset && typeof runtimeAsset === "object" && "uri" in runtimeAsset) {
23024
- const uri = runtimeAsset.uri;
23630
+ if (asset && typeof asset === "object" && "uri" in asset) {
23631
+ const uri = asset.uri;
23025
23632
  if (uri) return { uri };
23026
23633
  }
23027
23634
  return { html: "" };
23028
- })();
23635
+ };
23029
23636
  var WebViewViewer = ({ engine }) => {
23030
23637
  const webViewRef = useRef2(null);
23031
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]);
23032
23685
  useEffect2(() => {
23033
- const bridgeEngine = engine;
23034
23686
  bridgeEngine.attachWebView?.({
23035
23687
  postMessage: (message) => {
23036
23688
  if (__DEV__) {
@@ -23040,9 +23692,22 @@ var WebViewViewer = ({ engine }) => {
23040
23692
  webViewRef.current?.postMessage(message);
23041
23693
  }
23042
23694
  });
23043
- }, [engine]);
23695
+ }, [bridgeEngine]);
23044
23696
  const handleMessage = (event) => {
23045
- 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
+ }
23046
23711
  if (__DEV__) {
23047
23712
  console.log("[Papyrus WebView] message", event.nativeEvent.data);
23048
23713
  }
@@ -23082,11 +23747,12 @@ var WebViewViewer = ({ engine }) => {
23082
23747
  WebView,
23083
23748
  {
23084
23749
  ref: webViewRef,
23085
- source: runtimeSource,
23750
+ source: webViewSource,
23086
23751
  originWhitelist: ["*"],
23087
23752
  onMessage: handleMessage,
23088
23753
  onLoadEnd: handleLoadEnd,
23089
23754
  onError: handleError,
23755
+ injectedJavaScriptBeforeContentLoaded: runtimeConfigScript,
23090
23756
  javaScriptEnabled: true,
23091
23757
  domStorageEnabled: true,
23092
23758
  scalesPageToFit: true,
@@ -23136,7 +23802,7 @@ var styles2 = StyleSheet2.create({
23136
23802
  var WebViewViewer_default = WebViewViewer;
23137
23803
 
23138
23804
  // components/DedicatedAndroidPdfViewer.tsx
23139
- 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";
23140
23806
  import { Pressable as Pressable2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
23141
23807
  import { useViewerStore as useViewerStore3 } from "@papyrus-sdk/core";
23142
23808
  import { PapyrusPdfDocumentView } from "@papyrus-sdk/engine-native";
@@ -23782,7 +24448,7 @@ function DedicatedAndroidPdfViewer({
23782
24448
  const addAnnotation = useViewerStore3((state) => state.addAnnotation);
23783
24449
  const setSelectedAnnotation = useViewerStore3((state) => state.setSelectedAnnotation);
23784
24450
  const engineId = getDedicatedAndroidPdfEngineId(engine);
23785
- const [selection, setSelection] = useState2(null);
24451
+ const [selection, setSelection] = useState3(null);
23786
24452
  const selectionRef = useRef3(null);
23787
24453
  const lastVisiblePagesKeyRef = useRef3("");
23788
24454
  const lastScrollOffsetYRef = useRef3(0);
@@ -24135,13 +24801,13 @@ var Viewer = ({
24135
24801
  const lastScrollOffsetYRef = useRef4(0);
24136
24802
  const scrollDownAccumRef = useRef4(0);
24137
24803
  const scrollUpAccumRef = useRef4(0);
24138
- const [layoutRevision, setLayoutRevision] = useState3(0);
24139
- const [selectionDragActive, setSelectionDragActive] = useState3(false);
24804
+ const [layoutRevision, setLayoutRevision] = useState4(0);
24805
+ const [selectionDragActive, setSelectionDragActive] = useState4(false);
24140
24806
  const selectionDragActiveRef = useRef4(false);
24141
- const [gestureScrollLockActive, setGestureScrollLockActive] = useState3(false);
24807
+ const [gestureScrollLockActive, setGestureScrollLockActive] = useState4(false);
24142
24808
  const gestureScrollLockActiveRef = useRef4(false);
24143
- const [pinchPreviewScale, setPinchPreviewScale] = useState3(1);
24144
- const [lastPinchEndedAt, setLastPinchEndedAt] = useState3(null);
24809
+ const [pinchPreviewScale, setPinchPreviewScale] = useState4(1);
24810
+ const [lastPinchEndedAt, setLastPinchEndedAt] = useState4(null);
24145
24811
  const pinchGestureActiveRef = useRef4(false);
24146
24812
  const pinchStartZoomRef = useRef4(1);
24147
24813
  const pinchPreviewZoomRef = useRef4(1);
@@ -25474,7 +26140,7 @@ var styles4 = StyleSheet4.create({
25474
26140
  var Viewer_default = Viewer;
25475
26141
 
25476
26142
  // components/Topbar.tsx
25477
- import { useState as useState5 } from "react";
26143
+ import { useState as useState6 } from "react";
25478
26144
  import { View as View6, Text as Text2, Pressable as Pressable4, StyleSheet as StyleSheet6, useWindowDimensions as useWindowDimensions3 } from "react-native";
25479
26145
  import { useViewerStore as useViewerStore5 } from "@papyrus-sdk/core";
25480
26146
 
@@ -25497,7 +26163,7 @@ var MOBILE_CHROME_METRICS = {
25497
26163
  };
25498
26164
 
25499
26165
  // components/PageJumpModal.tsx
25500
- import { useEffect as useEffect5, useState as useState4 } from "react";
26166
+ import { useEffect as useEffect5, useState as useState5 } from "react";
25501
26167
  import { Modal, Pressable as Pressable3, StyleSheet as StyleSheet5, Text, TextInput, View as View5 } from "react-native";
25502
26168
 
25503
26169
  // components/pageJumpModel.ts
@@ -25519,7 +26185,7 @@ function PageJumpModal({
25519
26185
  onClose,
25520
26186
  onConfirm
25521
26187
  }) {
25522
- const [value, setValue] = useState4(`${currentPage}`);
26188
+ const [value, setValue] = useState5(`${currentPage}`);
25523
26189
  useEffect5(() => {
25524
26190
  if (visible) setValue(`${currentPage}`);
25525
26191
  }, [currentPage, visible]);
@@ -25680,7 +26346,7 @@ var Topbar = ({
25680
26346
  accentColor,
25681
26347
  mobileChromeVisible
25682
26348
  } = useViewerStore5();
25683
- const [jumpModalOpen, setJumpModalOpen] = useState5(false);
26349
+ const [jumpModalOpen, setJumpModalOpen] = useState6(false);
25684
26350
  const isDark = uiTheme === "dark";
25685
26351
  const navIconColor = isDark ? "#e5e7eb" : "#111827";
25686
26352
  const { width, height } = useWindowDimensions3();
@@ -25983,7 +26649,7 @@ var styles6 = StyleSheet6.create({
25983
26649
  var Topbar_default = Topbar;
25984
26650
 
25985
26651
  // components/ToolDock.tsx
25986
- 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";
25987
26653
  import {
25988
26654
  View as View7,
25989
26655
  Text as Text3,
@@ -26221,8 +26887,8 @@ var OpacitySlider = React7.memo(function OpacitySlider2({
26221
26887
  isDark,
26222
26888
  onCommit
26223
26889
  }) {
26224
- const [trackWidth, setTrackWidth] = useState6(0);
26225
- const [draftValue, setDraftValue] = useState6(value);
26890
+ const [trackWidth, setTrackWidth] = useState7(0);
26891
+ const [draftValue, setDraftValue] = useState7(value);
26226
26892
  const dragValueRef = useRef5(draftValue);
26227
26893
  const isDraggingRef = useRef5(false);
26228
26894
  const frameRef = useRef5(null);
@@ -26331,8 +26997,8 @@ var ToolDock = () => {
26331
26997
  undoAnnotations,
26332
26998
  redoAnnotations
26333
26999
  } = useViewerStore6();
26334
- const [paletteExpanded, setPaletteExpanded] = useState6(false);
26335
- const [extrasExpanded, setExtrasExpanded] = useState6(false);
27000
+ const [paletteExpanded, setPaletteExpanded] = useState7(false);
27001
+ const [extrasExpanded, setExtrasExpanded] = useState7(false);
26336
27002
  const isDark = uiTheme === "dark";
26337
27003
  const { width: windowWidth } = useWindowDimensions4();
26338
27004
  const t = getStrings(locale);
@@ -27055,9 +27721,10 @@ var withInkAlpha = (hex, alpha) => {
27055
27721
  var ToolDock_default = ToolDock;
27056
27722
 
27057
27723
  // components/RightSheet.tsx
27058
- 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";
27059
27725
  import {
27060
27726
  Dimensions,
27727
+ Image as Image2,
27061
27728
  Platform as Platform3,
27062
27729
  Pressable as Pressable7,
27063
27730
  StyleSheet as StyleSheet9,
@@ -27248,11 +27915,13 @@ var PageThumbnail = ({
27248
27915
  frameHeight,
27249
27916
  accentColor,
27250
27917
  useNativePreview,
27918
+ useImagePreview,
27251
27919
  shouldRenderPreview,
27252
27920
  onPress
27253
27921
  }) => {
27254
27922
  const viewRef = useRef6(null);
27255
- const [layoutReady, setLayoutReady] = useState7(false);
27923
+ const [layoutReady, setLayoutReady] = useState8(false);
27924
+ const [previewUri, setPreviewUri] = useState8(null);
27256
27925
  useEffect7(() => {
27257
27926
  if (!layoutReady || !useNativePreview || !shouldRenderPreview) return;
27258
27927
  const viewTag = findNodeHandle2(viewRef.current);
@@ -27268,6 +27937,24 @@ var PageThumbnail = ({
27268
27937
  useNativePreview,
27269
27938
  zoom
27270
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]);
27271
27958
  const handleLayout = (event) => {
27272
27959
  if (event.nativeEvent.layout.width && event.nativeEvent.layout.height) {
27273
27960
  setLayoutReady(true);
@@ -27290,7 +27977,14 @@ var PageThumbnail = ({
27290
27977
  {
27291
27978
  onLayout: handleLayout,
27292
27979
  style: [styles9.thumbFrame, { width: frameWidth, height: frameHeight }],
27293
- 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(
27294
27988
  Text5,
27295
27989
  {
27296
27990
  style: [
@@ -27367,9 +28061,14 @@ var RightSheet = ({
27367
28061
  locale,
27368
28062
  accentColor
27369
28063
  } = useViewerStore7();
27370
- const [pagesMode, setPagesMode] = useState7(
27371
- documentType === "pdf" ? "thumbnails" : "summary"
28064
+ const [pagesMode, setPagesMode] = useState8(
28065
+ documentType === "pdf" || documentType === "comic" ? "thumbnails" : "summary"
27372
28066
  );
28067
+ useEffect7(() => {
28068
+ setPagesMode(
28069
+ documentType === "pdf" || documentType === "comic" ? "thumbnails" : "summary"
28070
+ );
28071
+ }, [documentType]);
27373
28072
  const isDark = uiTheme === "dark";
27374
28073
  const t = getStrings(locale);
27375
28074
  const showingNotes = sidebarRightTab === "annotations";
@@ -27387,11 +28086,12 @@ var RightSheet = ({
27387
28086
  UIManager.getViewManagerConfig?.("PapyrusPageView")
27388
28087
  );
27389
28088
  const useNativePreview = renderTarget !== "webview" && hasNativePageView;
28089
+ const useImagePreview = documentType === "comic" && renderTarget === "webview" && typeof engine.getPagePreview === "function";
27390
28090
  const thumbnailDimensionsCacheRef = useRef6(/* @__PURE__ */ new Map());
27391
28091
  const thumbnailDimensionsPendingRef = useRef6(/* @__PURE__ */ new Set());
27392
28092
  const thumbnailRefreshTimeoutRef = useRef6(null);
27393
- const [thumbnailLayoutRevision, setThumbnailLayoutRevision] = useState7(0);
27394
- const [visibleThumbnailPages, setVisibleThumbnailPages] = useState7(
28093
+ const [thumbnailLayoutRevision, setThumbnailLayoutRevision] = useState8(0);
28094
+ const [visibleThumbnailPages, setVisibleThumbnailPages] = useState8(
27395
28095
  () => /* @__PURE__ */ new Set()
27396
28096
  );
27397
28097
  const resolvedThumbsInitialCount = useMemo5(
@@ -27543,7 +28243,7 @@ var RightSheet = ({
27543
28243
  );
27544
28244
  const renderThumbnailItem = useCallback4(
27545
28245
  ({ item }) => {
27546
- 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);
27547
28247
  return /* @__PURE__ */ jsx10(
27548
28248
  PageThumbnail,
27549
28249
  {
@@ -27557,6 +28257,7 @@ var RightSheet = ({
27557
28257
  frameHeight: getThumbnailFrameHeight(item),
27558
28258
  accentColor,
27559
28259
  useNativePreview,
28260
+ useImagePreview,
27560
28261
  shouldRenderPreview,
27561
28262
  onPress: () => jumpToPage(item)
27562
28263
  }
@@ -27573,6 +28274,7 @@ var RightSheet = ({
27573
28274
  jumpToPage,
27574
28275
  resolvedThumbsPrewarmCount,
27575
28276
  useNativePreview,
28277
+ useImagePreview,
27576
28278
  visibleThumbnailPages,
27577
28279
  zoom
27578
28280
  ]
@@ -27904,6 +28606,10 @@ var styles9 = StyleSheet9.create({
27904
28606
  width: "100%",
27905
28607
  height: "100%"
27906
28608
  },
28609
+ thumbImage: {
28610
+ width: "100%",
28611
+ height: "100%"
28612
+ },
27907
28613
  thumbFallback: {
27908
28614
  flex: 1,
27909
28615
  alignItems: "center",
@@ -28044,7 +28750,7 @@ var styles9 = StyleSheet9.create({
28044
28750
  var RightSheet_default = RightSheet;
28045
28751
 
28046
28752
  // components/AnnotationEditor.tsx
28047
- import { useEffect as useEffect8, useState as useState8 } from "react";
28753
+ import { useEffect as useEffect8, useState as useState9 } from "react";
28048
28754
  import {
28049
28755
  Modal as Modal3,
28050
28756
  View as View10,
@@ -28067,7 +28773,7 @@ var AnnotationEditor = () => {
28067
28773
  } = useViewerStore8();
28068
28774
  const annotation = annotations.find((ann) => ann.id === selectedAnnotationId);
28069
28775
  const isEditable = annotation && (annotation.type === "text" || annotation.type === "comment");
28070
- const [draft, setDraft] = useState8("");
28776
+ const [draft, setDraft] = useState9("");
28071
28777
  const isDark = uiTheme === "dark";
28072
28778
  const t = getStrings(locale);
28073
28779
  useEffect8(() => {
@@ -29050,7 +29756,7 @@ var styles12 = StyleSheet12.create({
29050
29756
  var SettingsSheet_default = SettingsSheet;
29051
29757
 
29052
29758
  // components/CoverPreview.tsx
29053
- 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";
29054
29760
  import {
29055
29761
  ActivityIndicator,
29056
29762
  StyleSheet as StyleSheet13,
@@ -29108,11 +29814,11 @@ var CoverPreview = ({
29108
29814
  onLoadEnd,
29109
29815
  onError
29110
29816
  }) => {
29111
- const [engine, setEngine] = useState9(null);
29112
- const [layoutReady, setLayoutReady] = useState9(false);
29113
- const [loaded, setLoaded] = useState9(false);
29114
- const [loading, setLoading] = useState9(false);
29115
- 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);
29116
29822
  const viewRef = useRef7(null);
29117
29823
  const resolvedType = useMemo6(() => inferDocumentType(source, type), [source, type]);
29118
29824
  const isPdf = resolvedType === "pdf";
@@ -29232,7 +29938,7 @@ var styles13 = StyleSheet13.create({
29232
29938
  var CoverPreview_default = CoverPreview;
29233
29939
 
29234
29940
  // components/ReadingShell.tsx
29235
- import React13, { useState as useState11 } from "react";
29941
+ import React13, { useState as useState12 } from "react";
29236
29942
  import { Keyboard as Keyboard2, View as View20, StyleSheet as StyleSheet20 } from "react-native";
29237
29943
  import { useViewerStore as useViewerStore17 } from "@papyrus-sdk/core";
29238
29944
 
@@ -29857,7 +30563,7 @@ var styles17 = StyleSheet17.create({
29857
30563
  });
29858
30564
 
29859
30565
  // components/SearchOverlay.tsx
29860
- 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";
29861
30567
  import {
29862
30568
  Keyboard,
29863
30569
  Pressable as Pressable15,
@@ -29886,8 +30592,8 @@ function SearchOverlay({
29886
30592
  uiTheme,
29887
30593
  accentColor
29888
30594
  } = useViewerStore15();
29889
- const [draft, setDraft] = useState10(searchQuery);
29890
- const [isSearching, setIsSearching] = useState10(false);
30595
+ const [draft, setDraft] = useState11(searchQuery);
30596
+ const [isSearching, setIsSearching] = useState11(false);
29891
30597
  const isDark = uiTheme === "dark";
29892
30598
  const t = getStrings(locale);
29893
30599
  const searchService = useMemo8(() => new SearchService(engine), [engine]);
@@ -30378,12 +31084,12 @@ function ReadingShell({
30378
31084
  uiTheme,
30379
31085
  accentColor
30380
31086
  } = useViewerStore17();
30381
- const [overflowOpen, setOverflowOpen] = useState11(false);
30382
- const [infoOpen, setInfoOpen] = useState11(false);
30383
- const [actionsOpen, setActionsOpen] = useState11(false);
30384
- const [settingsOpen, setSettingsOpen] = useState11(false);
30385
- const [searchResultsOpen, setSearchResultsOpen] = useState11(false);
30386
- 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);
30387
31093
  const isDark = uiTheme === "dark";
30388
31094
  React13.useEffect(() => {
30389
31095
  const showSubscription = Keyboard2.addListener("keyboardDidShow", () => {