@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.
@@ -20369,9 +20369,121 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20369
20369
  ]).default;
20370
20370
  });
20371
20371
  </script>
20372
+ <!-- @papyrus-comic-runtime:start -->
20373
+ <script>
20374
+ (function (root, factory) {
20375
+ if (typeof module === "object" && module.exports) {
20376
+ module.exports = factory();
20377
+ } else {
20378
+ root.PapyrusComicRuntime = Object.assign(
20379
+ factory(),
20380
+ root.PapyrusComicRuntime || {}
20381
+ );
20382
+ }
20383
+ })(typeof self === "object" ? self : this, function () {
20384
+ var IMAGE_EXTENSIONS = /\.(?:gif|jpe?g|png|svg|webp)$/i;
20385
+
20386
+ function isComicImageName(name) {
20387
+ return typeof name === "string" && IMAGE_EXTENSIONS.test(name);
20388
+ }
20389
+
20390
+ function naturalCompare(left, right) {
20391
+ var leftParts = String(left).split(/(\d+)/);
20392
+ var rightParts = String(right).split(/(\d+)/);
20393
+ var length = Math.max(leftParts.length, rightParts.length);
20394
+
20395
+ for (var index = 0; index < length; index += 1) {
20396
+ var leftPart = leftParts[index] || "";
20397
+ var rightPart = rightParts[index] || "";
20398
+ if (leftPart === rightPart) continue;
20399
+
20400
+ var leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
20401
+ var rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
20402
+ if (leftNumber !== null && rightNumber !== null) {
20403
+ return leftNumber - rightNumber;
20404
+ }
20405
+ return leftPart.localeCompare(rightPart, undefined, {
20406
+ numeric: true,
20407
+ sensitivity: "base",
20408
+ });
20409
+ }
20410
+
20411
+ return 0;
20412
+ }
20413
+
20414
+ function sortComicPageNames(names) {
20415
+ return names.slice().sort(naturalCompare);
20416
+ }
20417
+
20418
+ function patchCbrWorkerSource(workerSource, wasmUrl) {
20419
+ return workerSource.replace(
20420
+ /new URL\("libarchive\.wasm",import\.meta\.url\)\.href/g,
20421
+ JSON.stringify(wasmUrl)
20422
+ );
20423
+ }
20424
+
20425
+ function getComicPreviewSize(width, height, maxWidth, maxHeight) {
20426
+ var safeWidth = Math.max(1, Number(width) || 1);
20427
+ var safeHeight = Math.max(1, Number(height) || 1);
20428
+ var widthLimit = maxWidth || 240;
20429
+ var heightLimit = maxHeight || 360;
20430
+ var scale = Math.min(1, widthLimit / safeWidth, heightLimit / safeHeight);
20431
+ return {
20432
+ width: Math.max(1, Math.round(safeWidth * scale)),
20433
+ height: Math.max(1, Math.round(safeHeight * scale)),
20434
+ };
20435
+ }
20436
+
20437
+ function getComicPageAspectRatio(width, height) {
20438
+ var safeWidth = Math.max(1, Number(width) || 1);
20439
+ var safeHeight = Math.max(1, Number(height) || 1);
20440
+ return safeWidth + " / " + safeHeight;
20441
+ }
20442
+
20443
+ function getProtectedComicPageIndexes(
20444
+ currentPage,
20445
+ visiblePageIndexes,
20446
+ loadingPageIndexes
20447
+ ) {
20448
+ var protectedIndexes = new Set([
20449
+ currentPage - 1,
20450
+ currentPage,
20451
+ currentPage - 2,
20452
+ ]);
20453
+ (visiblePageIndexes || []).forEach(function (pageIndex) {
20454
+ protectedIndexes.add(pageIndex);
20455
+ });
20456
+ (loadingPageIndexes || []).forEach(function (pageIndex) {
20457
+ protectedIndexes.add(pageIndex);
20458
+ });
20459
+ return protectedIndexes;
20460
+ }
20461
+
20462
+ function isCurrentComicPageLoad(
20463
+ loadGeneration,
20464
+ currentGeneration,
20465
+ entry,
20466
+ currentEntry
20467
+ ) {
20468
+ return loadGeneration === currentGeneration && entry === currentEntry;
20469
+ }
20470
+
20471
+ return {
20472
+ isComicImageName: isComicImageName,
20473
+ sortComicPageNames: sortComicPageNames,
20474
+ patchCbrWorkerSource: patchCbrWorkerSource,
20475
+ getComicPreviewSize: getComicPreviewSize,
20476
+ getComicPageAspectRatio: getComicPageAspectRatio,
20477
+ getProtectedComicPageIndexes: getProtectedComicPageIndexes,
20478
+ isCurrentComicPageLoad: isCurrentComicPageLoad,
20479
+ };
20480
+ });
20481
+
20482
+ </script>
20483
+ <!-- @papyrus-comic-runtime:end -->
20372
20484
  <script>
20373
20485
  (function () {
20374
- const viewer = document.getElementById("viewer");
20486
+ const viewer = document.getElementById('viewer');
20375
20487
  const DEFAULT_FONT_SIZE = 16;
20376
20488
  const TEXT_PAGE_CHUNK = 1600;
20377
20489
 
@@ -20384,27 +20496,116 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20384
20496
  let currentPage = 1;
20385
20497
  let pageCount = 0;
20386
20498
  let zoom = 1.0;
20499
+ let rotation = 0;
20500
+ let comicArchive = null;
20501
+ let comicEntries = [];
20502
+ let comicContainer = null;
20503
+ let comicPages = [];
20504
+ let comicObserver = null;
20505
+ let comicScrollFrame = null;
20506
+ const comicObjectUrls = new Map();
20507
+ const comicLoading = new Map();
20508
+ const comicDimensions = new Map();
20509
+ const comicPreviewCache = new Map();
20510
+ const comicPreviewLoading = new Map();
20511
+ const COMIC_CACHE_LIMIT = 16;
20512
+ const COMIC_PREVIEW_CACHE_LIMIT = 8;
20513
+ const comicHelpers = window.PapyrusComicRuntime || {};
20514
+ const runtimeConfig = window.__PAPYRUS_RUNTIME_CONFIG__ || {};
20515
+ let comicDispose = null;
20516
+ let comicGeneration = 0;
20387
20517
 
20388
20518
  const sendMessage = (payload) => {
20389
- if (
20390
- window.ReactNativeWebView &&
20391
- window.ReactNativeWebView.postMessage
20392
- ) {
20519
+ if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
20393
20520
  window.ReactNativeWebView.postMessage(JSON.stringify(payload));
20394
20521
  return;
20395
20522
  }
20396
20523
  if (window.parent && window.parent !== window) {
20397
- window.parent.postMessage(payload, "*");
20524
+ window.parent.postMessage(payload, '*');
20398
20525
  }
20399
20526
  };
20400
20527
 
20401
20528
  const sendResponse = (id, ok, data, error) => {
20402
- sendMessage({ type: "response", id, ok, data, error });
20529
+ sendMessage({ type: 'response', id, ok, data, error });
20530
+ };
20531
+
20532
+ const runtimeAssetRequests = new Map();
20533
+ const fileChunkRequests = new Map();
20534
+
20535
+ const requestRuntimeAsset = (url, encoding) =>
20536
+ new Promise((resolve, reject) => {
20537
+ const id = `runtime-asset-${Date.now()}-${Math.random()}`;
20538
+ const timeout = setTimeout(() => {
20539
+ runtimeAssetRequests.delete(id);
20540
+ reject(new Error(`Tempo esgotado ao carregar asset CBR: ${url}`));
20541
+ }, 15000);
20542
+ runtimeAssetRequests.set(id, {resolve, reject, timeout});
20543
+ sendMessage({type: 'asset-request', id, url, encoding});
20544
+ });
20545
+
20546
+ const encodeArrayBufferAsDataUrl = async (buffer) => {
20547
+ const bytes = new Uint8Array(buffer);
20548
+ let binary = '';
20549
+ const chunkSize = 0x8000;
20550
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
20551
+ binary += String.fromCharCode(
20552
+ ...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),
20553
+ );
20554
+ }
20555
+ return `data:application/wasm;base64,${btoa(binary)}`;
20556
+ };
20557
+
20558
+ const fetchRuntimeAsset = async (url, encoding) => {
20559
+ try {
20560
+ const response = await fetch(url);
20561
+ if (!response.ok) throw new Error(`status ${response.status}`);
20562
+ if (encoding === 'text') return await response.text();
20563
+ return await encodeArrayBufferAsDataUrl(await response.arrayBuffer());
20564
+ } catch {
20565
+ return await requestRuntimeAsset(url, encoding);
20566
+ }
20567
+ };
20568
+
20569
+ const requestLocalFileChunk = (uri, offset, length) =>
20570
+ new Promise((resolve, reject) => {
20571
+ const id = `file-chunk-${Date.now()}-${Math.random()}`;
20572
+ const timeout = setTimeout(() => {
20573
+ fileChunkRequests.delete(id);
20574
+ reject(new Error(`Tempo esgotado ao ler arquivo local: ${uri}`));
20575
+ }, 30000);
20576
+ fileChunkRequests.set(id, {resolve, reject, timeout});
20577
+ sendMessage({type: 'file-chunk-request', id, uri, offset, length});
20578
+ });
20579
+
20580
+ const readLocalFile = async (uri) => {
20581
+ const chunkSize = 1024 * 1024;
20582
+ const chunks = [];
20583
+ let offset = 0;
20584
+ let totalLength = 0;
20585
+ while (true) {
20586
+ const result = await requestLocalFileChunk(uri, offset, chunkSize);
20587
+ const bytes = decodeBase64(result.data || '');
20588
+ if (bytes.length === 0 && !result.done) {
20589
+ throw new Error('Leitura nativa retornou um chunk vazio.');
20590
+ }
20591
+ chunks.push(bytes);
20592
+ totalLength += bytes.length;
20593
+ offset += bytes.length;
20594
+ if (result.done) break;
20595
+ }
20596
+
20597
+ const output = new Uint8Array(totalLength);
20598
+ let outputOffset = 0;
20599
+ chunks.forEach((chunk) => {
20600
+ output.set(chunk, outputOffset);
20601
+ outputOffset += chunk.length;
20602
+ });
20603
+ return output.buffer;
20403
20604
  };
20404
20605
 
20405
20606
  const sendState = (extra) => {
20406
20607
  sendMessage({
20407
- type: "state",
20608
+ type: 'state',
20408
20609
  payload: {
20409
20610
  pageCount,
20410
20611
  currentPage,
@@ -20415,70 +20616,66 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20415
20616
  };
20416
20617
 
20417
20618
  const sendEvent = (name, payload) => {
20418
- sendMessage({ type: "event", name, payload });
20419
- };
20420
-
20421
- const log = (...args) => {
20422
- sendEvent("RUNTIME_LOG", {
20423
- message: args.map((value) => String(value)).join(" "),
20424
- });
20619
+ sendMessage({ type: 'event', name, payload });
20425
20620
  };
20426
20621
 
20427
20622
  const reportError = (error, context) => {
20428
- const message =
20429
- error && error.message ? error.message : String(error);
20623
+ const message = error && error.message ? error.message : String(error);
20430
20624
  const stack = error && error.stack ? error.stack : null;
20431
- sendEvent("RUNTIME_ERROR", { message, context, stack });
20432
- };
20433
-
20434
- const EPUB_OPEN_TIMEOUT_MS = 180000;
20435
- const EPUB_READY_TIMEOUT_MS = 180000;
20436
- const EPUB_DISPLAY_TIMEOUT_MS = 30000;
20437
-
20438
- const withTimeout = async (promise, ms, label) => {
20439
- let timer = null;
20440
- try {
20441
- return await Promise.race([
20442
- promise,
20443
- new Promise((_, reject) => {
20444
- timer = setTimeout(
20445
- () => reject(new Error(`Timeout: ${label}`)),
20446
- ms
20447
- );
20448
- }),
20449
- ]);
20450
- } finally {
20451
- if (timer) {
20452
- clearTimeout(timer);
20453
- }
20454
- }
20625
+ sendEvent('RUNTIME_ERROR', { message, context, stack });
20455
20626
  };
20456
20627
 
20457
20628
  const shouldIgnoreError = (error) => {
20458
- const message =
20459
- error && error.message ? error.message : String(error || "");
20460
- return message.includes("ResizeObserver loop");
20629
+ const message = error && error.message ? error.message : String(error || '');
20630
+ return message.includes('ResizeObserver loop');
20461
20631
  };
20462
20632
 
20463
- window.addEventListener("error", (event) => {
20633
+ window.addEventListener('error', (event) => {
20464
20634
  const error = event.error || event.message;
20465
20635
  if (shouldIgnoreError(error)) return;
20466
- reportError(error, "window.error");
20636
+ reportError(error, 'window.error');
20467
20637
  });
20468
20638
 
20469
- window.addEventListener("unhandledrejection", (event) => {
20639
+ window.addEventListener('unhandledrejection', (event) => {
20470
20640
  if (shouldIgnoreError(event.reason)) return;
20471
- reportError(event.reason, "unhandledrejection");
20641
+ reportError(event.reason, 'unhandledrejection');
20472
20642
  });
20473
20643
 
20644
+ const clearComicState = () => {
20645
+ comicGeneration += 1;
20646
+ if (comicObserver) {
20647
+ comicObserver.disconnect();
20648
+ comicObserver = null;
20649
+ }
20650
+ if (comicScrollFrame !== null) {
20651
+ cancelAnimationFrame(comicScrollFrame);
20652
+ comicScrollFrame = null;
20653
+ }
20654
+ comicObjectUrls.forEach((url) => URL.revokeObjectURL(url));
20655
+ comicObjectUrls.clear();
20656
+ comicLoading.clear();
20657
+ comicDimensions.clear();
20658
+ comicPreviewCache.clear();
20659
+ comicPreviewLoading.clear();
20660
+ if (comicDispose) {
20661
+ void comicDispose();
20662
+ comicDispose = null;
20663
+ }
20664
+ comicArchive = null;
20665
+ comicEntries = [];
20666
+ comicContainer = null;
20667
+ comicPages = [];
20668
+ };
20669
+
20474
20670
  const clearViewer = () => {
20671
+ clearComicState();
20475
20672
  while (viewer.firstChild) {
20476
20673
  viewer.removeChild(viewer.firstChild);
20477
20674
  }
20478
20675
  };
20479
20676
 
20480
20677
  const decodeBase64 = (value) => {
20481
- const clean = value.replace(/\s/g, "");
20678
+ const clean = value.replace(/\s/g, '');
20482
20679
  const binary = atob(clean);
20483
20680
  const len = binary.length;
20484
20681
  const bytes = new Uint8Array(len);
@@ -20490,41 +20687,438 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20490
20687
 
20491
20688
  const decodeBase64ToText = (value) => {
20492
20689
  const bytes = decodeBase64(value);
20493
- return new TextDecoder("utf-8").decode(bytes);
20690
+ return new TextDecoder('utf-8').decode(bytes);
20691
+ };
20692
+
20693
+ const sourceToArrayBuffer = async (source) => {
20694
+ if (source.kind === 'uri') {
20695
+ if (/^(?:file|content):\/\//i.test(source.uri)) {
20696
+ return readLocalFile(source.uri);
20697
+ }
20698
+ const response = await fetch(source.uri);
20699
+ if (!response.ok && response.status !== 0) {
20700
+ throw new Error(`Falha ao carregar quadrinho (${response.status})`);
20701
+ }
20702
+ return response.arrayBuffer();
20703
+ }
20704
+ if (source.kind === 'base64') {
20705
+ return decodeBase64(source.data).buffer;
20706
+ }
20707
+ throw new Error('Fonte de quadrinho inválida.');
20494
20708
  };
20495
20709
 
20496
- const fetchUriAsArrayBuffer = async (uri) => {
20497
- const response = await fetch(uri);
20498
- const ok = response.ok || response.status === 0;
20499
- if (!ok) {
20500
- throw new Error(
20501
- "Failed to fetch epub uri (" + response.status + ")"
20710
+ const comicMimeType = (name) => {
20711
+ const extension = name.split('.').pop()?.toLowerCase();
20712
+ if (extension === 'png') return 'image/png';
20713
+ if (extension === 'gif') return 'image/gif';
20714
+ if (extension === 'svg') return 'image/svg+xml';
20715
+ if (extension === 'webp') return 'image/webp';
20716
+ return 'image/jpeg';
20717
+ };
20718
+
20719
+ const updateComicCurrentPage = () => {
20720
+ if (!comicContainer || comicPages.length === 0) return;
20721
+ const viewportTop = comicContainer.scrollTop;
20722
+ let closestIndex = currentPage - 1;
20723
+ let closestDistance = Number.POSITIVE_INFINITY;
20724
+ comicPages.forEach((page, index) => {
20725
+ const distance = Math.abs(page.offsetTop - viewportTop);
20726
+ if (distance < closestDistance) {
20727
+ closestDistance = distance;
20728
+ closestIndex = index;
20729
+ }
20730
+ });
20731
+ const nextPage = Math.max(1, Math.min(pageCount, closestIndex + 1));
20732
+ if (nextPage !== currentPage) {
20733
+ currentPage = nextPage;
20734
+ sendState();
20735
+ }
20736
+ };
20737
+
20738
+ const scheduleComicCurrentPage = () => {
20739
+ if (comicScrollFrame !== null) return;
20740
+ comicScrollFrame = requestAnimationFrame(() => {
20741
+ comicScrollFrame = null;
20742
+ updateComicCurrentPage();
20743
+ });
20744
+ };
20745
+
20746
+ const evictComicUrls = () => {
20747
+ if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) return;
20748
+ const protectedIndexes = new Set([
20749
+ currentPage - 1,
20750
+ currentPage,
20751
+ currentPage - 2,
20752
+ ]);
20753
+ for (const [index, url] of comicObjectUrls) {
20754
+ if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) break;
20755
+ if (protectedIndexes.has(index)) continue;
20756
+ URL.revokeObjectURL(url);
20757
+ comicObjectUrls.delete(index);
20758
+ const image = comicPages[index]?.querySelector('img');
20759
+ const page = comicPages[index];
20760
+ const dimensions = comicDimensions.get(index);
20761
+ const width = dimensions?.width || image?.naturalWidth || 0;
20762
+ const height = dimensions?.height || image?.naturalHeight || 0;
20763
+ if (page && width > 0 && height > 0) {
20764
+ page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
20765
+ width,
20766
+ height
20767
+ );
20768
+ }
20769
+ if (image) image.removeAttribute('src');
20770
+ }
20771
+ };
20772
+
20773
+ const ensureComicPage = async (pageIndex) => {
20774
+ if (comicObjectUrls.has(pageIndex)) {
20775
+ const image = comicPages[pageIndex]?.querySelector('img');
20776
+ if (image && !image.src) image.src = comicObjectUrls.get(pageIndex);
20777
+ return comicObjectUrls.get(pageIndex);
20778
+ }
20779
+ if (comicLoading.has(pageIndex)) return comicLoading.get(pageIndex);
20780
+
20781
+ const entry = comicEntries[pageIndex];
20782
+ if (!entry) throw new Error(`Página de quadrinho inválida: ${pageIndex}`);
20783
+ const loadGeneration = comicGeneration;
20784
+ const pending = entry.async('blob').then((blob) => {
20785
+ if (
20786
+ !comicHelpers.isCurrentComicPageLoad(
20787
+ loadGeneration,
20788
+ comicGeneration,
20789
+ entry,
20790
+ comicEntries[pageIndex]
20791
+ )
20792
+ ) {
20793
+ return null;
20794
+ }
20795
+ const url = URL.createObjectURL(
20796
+ new Blob([blob], { type: comicMimeType(entry.name) })
20502
20797
  );
20798
+ comicObjectUrls.set(pageIndex, url);
20799
+ const image = comicPages[pageIndex]?.querySelector('img');
20800
+ if (image) image.src = url;
20801
+ evictComicUrls();
20802
+ return url;
20803
+ });
20804
+ comicLoading.set(pageIndex, pending);
20805
+ try {
20806
+ return await pending;
20807
+ } finally {
20808
+ if (comicLoading.get(pageIndex) === pending) {
20809
+ comicLoading.delete(pageIndex);
20810
+ }
20503
20811
  }
20504
- if (typeof response.arrayBuffer === "function") {
20505
- return response.arrayBuffer();
20812
+ };
20813
+
20814
+ const renderComicPage = (pageIndex) => {
20815
+ const image = comicPages[pageIndex]?.querySelector('img');
20816
+ if (!image) return;
20817
+ image.style.width = `${Math.max(40, Math.round(100 * zoom))}%`;
20818
+ image.style.transform = `rotate(${rotation}deg)`;
20819
+ };
20820
+
20821
+ const applyComicLayout = () => {
20822
+ comicPages.forEach((_, pageIndex) => renderComicPage(pageIndex));
20823
+ };
20824
+
20825
+ const loadComic = async (source, format) => {
20826
+ clearViewer();
20827
+ let entries;
20828
+ if (format === 'cbr') {
20829
+ const clientUrl = runtimeConfig.cbrClientUrl;
20830
+ const workerUrl = runtimeConfig.cbrWorkerUrl;
20831
+ const wasmUrl = runtimeConfig.cbrWasmUrl;
20832
+ if (!clientUrl || !workerUrl || !wasmUrl) {
20833
+ throw new Error(
20834
+ 'CBR no mobile exige o pacote opcional de runtime libarchive.'
20835
+ );
20836
+ }
20837
+
20838
+ const [clientSource, workerSource, wasmSource] = await Promise.all([
20839
+ fetchRuntimeAsset(clientUrl, 'text'),
20840
+ fetchRuntimeAsset(workerUrl, 'text'),
20841
+ runtimeConfig.cbrWasmDataUrl ||
20842
+ fetchRuntimeAsset(wasmUrl, 'base64'),
20843
+ ]);
20844
+ const patchedWorkerSource = comicHelpers.patchCbrWorkerSource(
20845
+ workerSource,
20846
+ wasmSource
20847
+ );
20848
+ const workerObjectUrl = URL.createObjectURL(
20849
+ new Blob([patchedWorkerSource], {type: 'text/javascript'})
20850
+ );
20851
+ const clientObjectUrl = URL.createObjectURL(
20852
+ new Blob([clientSource], { type: 'text/javascript' })
20853
+ );
20854
+ try {
20855
+ const archiveModule = await import(clientObjectUrl);
20856
+ const createCbrWorker = () => {
20857
+ const worker = new Worker(workerObjectUrl, {type: 'module'});
20858
+ worker.addEventListener('error', (event) => {
20859
+ sendEvent('RUNTIME_ERROR', {
20860
+ message: `CBR worker: ${event?.message || 'falha ao executar o worker'}`,
20861
+ context: 'comic.worker',
20862
+ });
20863
+ });
20864
+ worker.addEventListener('messageerror', () => {
20865
+ sendEvent('RUNTIME_ERROR', {
20866
+ message: 'CBR worker: falha ao transferir mensagem',
20867
+ context: 'comic.worker',
20868
+ });
20869
+ });
20870
+ return worker;
20871
+ };
20872
+ archiveModule.Archive.init({getWorker: createCbrWorker});
20873
+ const bytes = await sourceToArrayBuffer(source);
20874
+ const file = new Blob([bytes], {
20875
+ type: 'application/vnd.comicbook-rar',
20876
+ });
20877
+ const archive = await archiveModule.Archive.open(file);
20878
+ const files = await archive.getFilesArray();
20879
+ entries = files
20880
+ .filter(
20881
+ (item) => {
20882
+ const name = `${item?.path || ''}${item?.file?.name || ''}`;
20883
+ return item?.file && comicHelpers.isComicImageName(name);
20884
+ }
20885
+ )
20886
+ .map((item) => {
20887
+ const name = `${item.path || ''}${item.file.name}`;
20888
+ return {
20889
+ name,
20890
+ async: async () => item.file.extract(),
20891
+ };
20892
+ });
20893
+ comicDispose = async () => {
20894
+ await archive.close();
20895
+ URL.revokeObjectURL(workerObjectUrl);
20896
+ URL.revokeObjectURL(clientObjectUrl);
20897
+ };
20898
+ } catch (error) {
20899
+ URL.revokeObjectURL(workerObjectUrl);
20900
+ URL.revokeObjectURL(clientObjectUrl);
20901
+ throw error;
20902
+ }
20903
+ } else {
20904
+ if (!window.JSZip) {
20905
+ throw new Error('O runtime ZIP do leitor mobile não foi carregado.');
20906
+ }
20907
+
20908
+ const bytes = await sourceToArrayBuffer(source);
20909
+ comicArchive = await window.JSZip.loadAsync(bytes);
20910
+ entries = Object.keys(comicArchive.files)
20911
+ .filter((name) => {
20912
+ const entry = comicArchive.files[name];
20913
+ return !entry.dir && comicHelpers.isComicImageName(name);
20914
+ })
20915
+ .map((name) => comicArchive.files[name]);
20916
+ }
20917
+
20918
+ const names = comicHelpers.sortComicPageNames(
20919
+ entries.map((entry) => entry.name)
20920
+ );
20921
+ const entriesByName = new Map(entries.map((entry) => [entry.name, entry]));
20922
+ comicEntries = names.map((name) => entriesByName.get(name));
20923
+ if (comicEntries.length === 0) {
20924
+ if (comicDispose) {
20925
+ await comicDispose();
20926
+ comicDispose = null;
20927
+ }
20928
+ throw new Error('O arquivo não contém páginas de imagem.');
20506
20929
  }
20507
- const blob = await response.blob();
20508
- return new Promise((resolve, reject) => {
20930
+
20931
+ comicContainer = document.createElement('div');
20932
+ comicContainer.style.width = '100%';
20933
+ comicContainer.style.height = '100%';
20934
+ comicContainer.style.overflow = 'auto';
20935
+ comicContainer.style.padding = '16px 0 120px';
20936
+ comicContainer.style.boxSizing = 'border-box';
20937
+ comicContainer.style.background = '#ffffff';
20938
+ comicContainer.addEventListener('scroll', scheduleComicCurrentPage, {
20939
+ passive: true,
20940
+ });
20941
+ viewer.style.overflow = 'hidden';
20942
+ viewer.appendChild(comicContainer);
20943
+
20944
+ comicPages = comicEntries.map((entry, pageIndex) => {
20945
+ const page = document.createElement('div');
20946
+ page.dataset.pageIndex = String(pageIndex);
20947
+ page.style.width = '100%';
20948
+ page.style.minHeight = '80px';
20949
+ page.style.display = 'flex';
20950
+ page.style.justifyContent = 'center';
20951
+ page.style.alignItems = 'flex-start';
20952
+ page.style.padding = '0 12px 20px';
20953
+ page.style.boxSizing = 'border-box';
20954
+
20955
+ const image = document.createElement('img');
20956
+ image.alt = `Página ${pageIndex + 1}`;
20957
+ image.draggable = false;
20958
+ image.decoding = 'async';
20959
+ image.loading = 'lazy';
20960
+ image.style.display = 'block';
20961
+ image.style.maxWidth = 'none';
20962
+ image.style.height = 'auto';
20963
+ image.style.userSelect = 'none';
20964
+ image.addEventListener('load', () => {
20965
+ if (image.naturalWidth > 0 && image.naturalHeight > 0) {
20966
+ comicDimensions.set(pageIndex, {
20967
+ width: image.naturalWidth,
20968
+ height: image.naturalHeight,
20969
+ });
20970
+ page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
20971
+ image.naturalWidth,
20972
+ image.naturalHeight
20973
+ );
20974
+ }
20975
+ });
20976
+ page.appendChild(image);
20977
+ comicContainer.appendChild(page);
20978
+ return page;
20979
+ });
20980
+
20981
+ if (typeof IntersectionObserver === 'function') {
20982
+ comicObserver = new IntersectionObserver(
20983
+ (observations) => {
20984
+ observations.forEach((observation) => {
20985
+ if (!observation.isIntersecting) return;
20986
+ const pageIndex = Number(observation.target.dataset.pageIndex);
20987
+ void ensureComicPage(pageIndex).catch((error) =>
20988
+ sendEvent('RUNTIME_ERROR', {
20989
+ message: error?.message || String(error),
20990
+ context: 'comic.page',
20991
+ })
20992
+ );
20993
+ });
20994
+ scheduleComicCurrentPage();
20995
+ },
20996
+ { root: comicContainer, rootMargin: '600px 0px' }
20997
+ );
20998
+ comicPages.forEach((page) => comicObserver.observe(page));
20999
+ } else {
21000
+ for (let pageIndex = 0; pageIndex < Math.min(3, comicPages.length); pageIndex += 1) {
21001
+ void ensureComicPage(pageIndex);
21002
+ }
21003
+ }
21004
+ pageCount = comicPages.length;
21005
+ currentPage = 1;
21006
+ await ensureComicPage(0);
21007
+ applyComicLayout();
21008
+ return { pageCount };
21009
+ };
21010
+
21011
+ const readComicBlobAsDataUrl = (blob, mimeType) =>
21012
+ new Promise((resolve, reject) => {
20509
21013
  const reader = new FileReader();
20510
- reader.onerror = () =>
20511
- reject(new Error("Failed to read epub blob"));
21014
+ reader.onerror = () => reject(new Error('Falha ao gerar preview.'));
20512
21015
  reader.onload = () => resolve(reader.result);
20513
- reader.readAsArrayBuffer(blob);
21016
+ reader.readAsDataURL(new Blob([blob], { type: mimeType }));
21017
+ });
21018
+
21019
+ const createComicPagePreview = async (pageIndex) => {
21020
+ const entry = comicEntries[pageIndex];
21021
+ if (!entry) return null;
21022
+ const mimeType = comicMimeType(entry.name);
21023
+ const sourceUrl = await ensureComicPage(pageIndex);
21024
+ if (!sourceUrl) return null;
21025
+ try {
21026
+ const image = new Image();
21027
+ image.decoding = 'async';
21028
+ await new Promise((resolve, reject) => {
21029
+ image.onload = resolve;
21030
+ image.onerror = () => reject(new Error('Falha ao decodificar preview.'));
21031
+ image.src = sourceUrl;
21032
+ });
21033
+ const size = comicHelpers.getComicPreviewSize(
21034
+ image.naturalWidth || image.width,
21035
+ image.naturalHeight || image.height
21036
+ );
21037
+ const canvas = document.createElement('canvas');
21038
+ canvas.width = size.width;
21039
+ canvas.height = size.height;
21040
+ const context = canvas.getContext('2d');
21041
+ if (!context) throw new Error('Canvas indisponível para preview.');
21042
+ context.fillStyle = '#ffffff';
21043
+ context.fillRect(0, 0, size.width, size.height);
21044
+ context.drawImage(image, 0, 0, size.width, size.height);
21045
+ return canvas.toDataURL('image/jpeg', 0.78);
21046
+ } catch {
21047
+ try {
21048
+ const response = await fetch(sourceUrl);
21049
+ const blob = await response.blob();
21050
+ return await readComicBlobAsDataUrl(blob, mimeType);
21051
+ } catch {
21052
+ return null;
21053
+ }
21054
+ }
21055
+ };
21056
+
21057
+ const getComicPagePreview = async (pageIndex) => {
21058
+ const entry = comicEntries[pageIndex];
21059
+ if (!entry) return null;
21060
+ if (comicPreviewCache.has(pageIndex)) {
21061
+ return comicPreviewCache.get(pageIndex);
21062
+ }
21063
+ if (comicPreviewLoading.has(pageIndex)) {
21064
+ return comicPreviewLoading.get(pageIndex);
21065
+ }
21066
+ const loadGeneration = comicGeneration;
21067
+ const pending = createComicPagePreview(pageIndex).then((preview) => {
21068
+ if (
21069
+ !comicHelpers.isCurrentComicPageLoad(
21070
+ loadGeneration,
21071
+ comicGeneration,
21072
+ entry,
21073
+ comicEntries[pageIndex]
21074
+ )
21075
+ ) {
21076
+ return null;
21077
+ }
21078
+ comicPreviewCache.set(pageIndex, preview);
21079
+ while (comicPreviewCache.size > COMIC_PREVIEW_CACHE_LIMIT) {
21080
+ comicPreviewCache.delete(comicPreviewCache.keys().next().value);
21081
+ }
21082
+ return preview;
20514
21083
  });
21084
+ comicPreviewLoading.set(pageIndex, pending);
21085
+ try {
21086
+ return await pending;
21087
+ } finally {
21088
+ if (comicPreviewLoading.get(pageIndex) === pending) {
21089
+ comicPreviewLoading.delete(pageIndex);
21090
+ }
21091
+ }
21092
+ };
21093
+
21094
+ const getComicPageDimensions = async (pageIndex) => {
21095
+ const known = comicDimensions.get(pageIndex);
21096
+ if (known) return known;
21097
+ await ensureComicPage(pageIndex);
21098
+ const image = comicPages[pageIndex]?.querySelector('img');
21099
+ if (image && !image.complete) {
21100
+ await new Promise((resolve) => {
21101
+ image.addEventListener('load', resolve, { once: true });
21102
+ image.addEventListener('error', resolve, { once: true });
21103
+ });
21104
+ }
21105
+ return (
21106
+ comicDimensions.get(pageIndex) || {
21107
+ width: image?.naturalWidth || 0,
21108
+ height: image?.naturalHeight || 0,
21109
+ }
21110
+ );
20515
21111
  };
20516
21112
 
20517
21113
  const normalizeHref = (href) => {
20518
- if (!href) return "";
20519
- return href.split("#")[0];
21114
+ if (!href) return '';
21115
+ return href.split('#')[0];
20520
21116
  };
20521
21117
 
20522
21118
  const getSpineIndexByHref = (href) => {
20523
21119
  const normalized = normalizeHref(href);
20524
21120
  if (!normalized) return -1;
20525
- return spineItems.findIndex(
20526
- (item) => normalizeHref(item.href) === normalized
20527
- );
21121
+ return spineItems.findIndex((item) => normalizeHref(item.href) === normalized);
20528
21122
  };
20529
21123
 
20530
21124
  const buildOutline = async () => {
@@ -20533,11 +21127,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20533
21127
  const toc = nav && nav.toc ? nav.toc : [];
20534
21128
 
20535
21129
  const mapItem = (item) => {
20536
- const title = item.label || item.title || "";
20537
- const pageIndex = getSpineIndexByHref(item.href || "");
20538
- const children = Array.isArray(item.subitems)
20539
- ? item.subitems.map(mapItem)
20540
- : [];
21130
+ const title = item.label || item.title || '';
21131
+ const pageIndex = getSpineIndexByHref(item.href || '');
21132
+ const children = Array.isArray(item.subitems) ? item.subitems.map(mapItem) : [];
20541
21133
  const outlineItem = { title, pageIndex };
20542
21134
  if (children.length > 0) outlineItem.children = children;
20543
21135
  return outlineItem;
@@ -20549,10 +21141,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20549
21141
  const applyEpubZoom = () => {
20550
21142
  if (!rendition || !rendition.themes) return;
20551
21143
  const fontSize = `${Math.round(zoom * 100)}%`;
20552
- if (typeof rendition.themes.fontSize === "function") {
21144
+ if (typeof rendition.themes.fontSize === 'function') {
20553
21145
  rendition.themes.fontSize(fontSize);
20554
- } else if (typeof rendition.themes.override === "function") {
20555
- rendition.themes.override("font-size", fontSize);
21146
+ } else if (typeof rendition.themes.override === 'function') {
21147
+ rendition.themes.override('font-size', fontSize);
20556
21148
  }
20557
21149
  };
20558
21150
 
@@ -20563,14 +21155,13 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20563
21155
  };
20564
21156
 
20565
21157
  const renderTextPage = (pageIndex) => {
20566
- const text = textPages[pageIndex] || "";
21158
+ const text = textPages[pageIndex] || '';
20567
21159
  if (!textContainer) {
20568
- textContainer = document.createElement("div");
20569
- textContainer.style.padding = "24px";
20570
- textContainer.style.lineHeight = "1.6";
20571
- textContainer.style.whiteSpace = "pre-wrap";
20572
- textContainer.style.fontFamily =
20573
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
21160
+ textContainer = document.createElement('div');
21161
+ textContainer.style.padding = '24px';
21162
+ textContainer.style.lineHeight = '1.6';
21163
+ textContainer.style.whiteSpace = 'pre-wrap';
21164
+ textContainer.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
20574
21165
  viewer.appendChild(textContainer);
20575
21166
  }
20576
21167
  textContainer.textContent = text;
@@ -20582,18 +21173,18 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20582
21173
  for (let i = 0; i < text.length; i += TEXT_PAGE_CHUNK) {
20583
21174
  pages.push(text.slice(i, i + TEXT_PAGE_CHUNK));
20584
21175
  }
20585
- return pages.length > 0 ? pages : [""];
21176
+ return pages.length > 0 ? pages : [''];
20586
21177
  };
20587
21178
 
20588
21179
  const loadText = async (source) => {
20589
- let text = "";
20590
- if (source.kind === "uri") {
21180
+ let text = '';
21181
+ if (source.kind === 'uri') {
20591
21182
  const res = await fetch(source.uri);
20592
21183
  text = await res.text();
20593
- } else if (source.kind === "base64") {
21184
+ } else if (source.kind === 'base64') {
20594
21185
  text = decodeBase64ToText(source.data);
20595
- } else if (source.kind === "text") {
20596
- text = source.text || "";
21186
+ } else if (source.kind === 'text') {
21187
+ text = source.text || '';
20597
21188
  }
20598
21189
 
20599
21190
  clearViewer();
@@ -20606,44 +21197,25 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20606
21197
  };
20607
21198
 
20608
21199
  const loadEpub = async (source) => {
20609
- log("epub.load:start", source.kind);
20610
- if (rendition && typeof rendition.destroy === "function") {
21200
+ if (rendition && typeof rendition.destroy === 'function') {
20611
21201
  rendition.destroy();
20612
21202
  }
20613
- if (book && typeof book.destroy === "function") {
21203
+ if (book && typeof book.destroy === 'function') {
20614
21204
  book.destroy();
20615
21205
  }
20616
21206
 
20617
- if (source.kind === "base64") {
20618
- book = ePub();
20619
- await withTimeout(
20620
- book.open(source.data, "base64"),
20621
- EPUB_OPEN_TIMEOUT_MS,
20622
- "epub.open"
20623
- );
20624
- } else if (source.kind === "uri") {
20625
- log("epub.load:fetch-uri");
20626
- const data = await withTimeout(
20627
- fetchUriAsArrayBuffer(source.uri),
20628
- EPUB_OPEN_TIMEOUT_MS,
20629
- "epub.fetch"
20630
- );
20631
- const input = data instanceof Uint8Array ? data.buffer : data;
20632
- book = ePub(input, { openAs: "epub" });
20633
- } else {
20634
- let data = null;
20635
- if (source.kind === "text") {
20636
- const encoder = new TextEncoder();
20637
- data = encoder.encode(source.text || "").buffer;
20638
- }
20639
- const input = data instanceof Uint8Array ? data.buffer : data;
20640
- book = ePub(input, { openAs: "epub" });
20641
- }
20642
- if (book && book.opened) {
20643
- await withTimeout(book.opened, EPUB_OPEN_TIMEOUT_MS, "epub.opened");
21207
+ let data = null;
21208
+ if (source.kind === 'uri') {
21209
+ data = source.uri;
21210
+ } else if (source.kind === 'base64') {
21211
+ data = decodeBase64(source.data);
21212
+ } else if (source.kind === 'text') {
21213
+ const encoder = new TextEncoder();
21214
+ data = encoder.encode(source.text || '').buffer;
20644
21215
  }
20645
- await withTimeout(book.ready, EPUB_READY_TIMEOUT_MS, "epub.ready");
20646
- log("epub.load:ready");
21216
+
21217
+ book = ePub(data);
21218
+ await book.ready;
20647
21219
 
20648
21220
  spineItems = book.spine && book.spine.items ? book.spine.items : [];
20649
21221
  pageCount = spineItems.length;
@@ -20651,64 +21223,38 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20651
21223
 
20652
21224
  clearViewer();
20653
21225
  rendition = book.renderTo(viewer, {
20654
- width: "100%",
20655
- height: "100%",
20656
- flow: "paginated",
20657
- spread: "none",
21226
+ width: '100%',
21227
+ height: '100%',
21228
+ flow: 'paginated',
21229
+ spread: 'none',
20658
21230
  });
20659
21231
 
20660
21232
  if (rendition && rendition.hooks && rendition.hooks.content) {
20661
21233
  rendition.hooks.content.register((contents) => {
20662
- const frame =
20663
- contents && contents.document
20664
- ? contents.document.defaultView.frameElement
20665
- : null;
21234
+ const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
20666
21235
  if (frame) {
20667
- frame.setAttribute(
20668
- "sandbox",
20669
- "allow-scripts allow-same-origin"
20670
- );
21236
+ frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
20671
21237
  }
20672
21238
  });
20673
21239
  }
20674
21240
 
20675
- if (rendition && typeof rendition.on === "function") {
20676
- rendition.on("selected", (cfiRange, contents) => {
20677
- const selection =
20678
- contents && contents.window
20679
- ? contents.window.getSelection()
20680
- : null;
20681
- const text = selection ? selection.toString().trim() : "";
21241
+ if (rendition && typeof rendition.on === 'function') {
21242
+ rendition.on('selected', (cfiRange, contents) => {
21243
+ const selection = contents && contents.window ? contents.window.getSelection() : null;
21244
+ const text = selection ? selection.toString().trim() : '';
20682
21245
  if (text) {
20683
- sendEvent("TEXT_SELECTED", {
20684
- text,
20685
- pageIndex: Math.max(0, currentPage - 1),
20686
- });
21246
+ sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
20687
21247
  }
20688
- if (
20689
- rendition &&
20690
- rendition.annotations &&
20691
- typeof rendition.annotations.remove === "function"
20692
- ) {
20693
- rendition.annotations.remove(cfiRange, "highlight");
21248
+ if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
21249
+ rendition.annotations.remove(cfiRange, 'highlight');
20694
21250
  }
20695
21251
  });
20696
21252
  }
20697
21253
 
20698
- try {
20699
- await withTimeout(
20700
- displayEpubPage(0),
20701
- EPUB_DISPLAY_TIMEOUT_MS,
20702
- "epub.display"
20703
- );
20704
- log("epub.load:displayed");
20705
- } catch (err) {
20706
- reportError(err, "epub.display");
20707
- }
21254
+ await displayEpubPage(0);
20708
21255
  applyEpubZoom();
20709
21256
 
20710
21257
  const outline = await buildOutline();
20711
- log("epub.load:outline", outline.length);
20712
21258
  return { pageCount, outline };
20713
21259
  };
20714
21260
 
@@ -20723,41 +21269,34 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20723
21269
  };
20724
21270
 
20725
21271
  const getTextContent = async (pageIndex) => {
20726
- if (currentType === "text") {
20727
- const text = textPages[pageIndex] || "";
20728
- return [
20729
- {
20730
- str: text,
20731
- dir: "ltr",
20732
- width: 0,
20733
- height: 0,
20734
- transform: [1, 0, 0, 1, 0, 0],
20735
- fontName: "default",
20736
- },
20737
- ];
21272
+ if (currentType === 'text') {
21273
+ const text = textPages[pageIndex] || '';
21274
+ return [{
21275
+ str: text,
21276
+ dir: 'ltr',
21277
+ width: 0,
21278
+ height: 0,
21279
+ transform: [1, 0, 0, 1, 0, 0],
21280
+ fontName: 'default',
21281
+ }];
20738
21282
  }
20739
21283
 
20740
- if (currentType === "epub") {
21284
+ if (currentType === 'epub') {
20741
21285
  if (!book) return [];
20742
21286
  const item = spineItems[pageIndex];
20743
21287
  if (!item) return [];
20744
21288
  try {
20745
21289
  const section = book.spine.get(item.idref || item.href);
20746
- const text =
20747
- section && typeof section.text === "function"
20748
- ? await section.text()
20749
- : "";
21290
+ const text = section && typeof section.text === 'function' ? await section.text() : '';
20750
21291
  if (!text) return [];
20751
- return [
20752
- {
20753
- str: text,
20754
- dir: "ltr",
20755
- width: 0,
20756
- height: 0,
20757
- transform: [1, 0, 0, 1, 0, 0],
20758
- fontName: "default",
20759
- },
20760
- ];
21292
+ return [{
21293
+ str: text,
21294
+ dir: 'ltr',
21295
+ width: 0,
21296
+ height: 0,
21297
+ transform: [1, 0, 0, 1, 0, 0],
21298
+ fontName: 'default',
21299
+ }];
20761
21300
  } catch (err) {
20762
21301
  return [];
20763
21302
  }
@@ -20768,7 +21307,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20768
21307
 
20769
21308
  const getPageText = async (pageIndex) => {
20770
21309
  const items = await getTextContent(pageIndex);
20771
- return items.map((item) => item.str).join(" ");
21310
+ return items.map((item) => item.str).join(' ');
20772
21311
  };
20773
21312
 
20774
21313
  const searchText = async (query) => {
@@ -20784,11 +21323,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20784
21323
  while (pos !== -1) {
20785
21324
  const start = Math.max(0, pos - 20);
20786
21325
  const end = Math.min(text.length, pos + query.length + 20);
20787
- results.push({
20788
- pageIndex: i,
20789
- text: text.substring(start, end),
20790
- matchIndex,
20791
- });
21326
+ results.push({ pageIndex: i, text: text.substring(start, end), matchIndex });
20792
21327
  matchIndex += 1;
20793
21328
  pos = lower.indexOf(normalized, pos + 1);
20794
21329
  }
@@ -20804,11 +21339,11 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20804
21339
 
20805
21340
  const getPageIndex = (dest) => {
20806
21341
  if (!dest) return null;
20807
- if (typeof dest === "string") return getSpineIndexByHref(dest);
20808
- if (typeof dest !== "object") return null;
20809
- if (dest.kind === "href") return getSpineIndexByHref(dest.value);
20810
- if (dest.kind === "pageIndex") return dest.value;
20811
- if (dest.kind === "pageNumber") return Math.max(0, dest.value - 1);
21342
+ if (typeof dest === 'string') return getSpineIndexByHref(dest);
21343
+ if (typeof dest !== 'object') return null;
21344
+ if (dest.kind === 'href') return getSpineIndexByHref(dest.value);
21345
+ if (dest.kind === 'pageIndex') return dest.value;
21346
+ if (dest.kind === 'pageNumber') return Math.max(0, dest.value - 1);
20812
21347
  return null;
20813
21348
  };
20814
21349
 
@@ -20822,53 +21357,70 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20822
21357
  };
20823
21358
 
20824
21359
  const applyZoom = () => {
20825
- if (currentType === "text") {
21360
+ if (currentType === 'text') {
20826
21361
  applyTextZoom();
20827
- } else if (currentType === "epub") {
21362
+ } else if (currentType === 'epub') {
20828
21363
  applyEpubZoom();
21364
+ } else if (currentType === 'comic') {
21365
+ applyComicLayout();
20829
21366
  }
20830
21367
  };
20831
21368
 
20832
21369
  const handleCommand = async (message) => {
20833
21370
  const { id, kind, payload } = message;
20834
- log("command", kind);
20835
21371
 
20836
21372
  try {
20837
- if (kind === "load") {
21373
+ if (kind === 'load') {
20838
21374
  currentType = payload.type;
20839
21375
  zoom = 1.0;
20840
21376
 
20841
- if (currentType === "text") {
21377
+ if (currentType === 'text') {
20842
21378
  const result = await loadText(payload.source);
20843
21379
  sendState();
20844
21380
  sendResponse(id, true, result);
20845
21381
  return;
20846
21382
  }
20847
21383
 
20848
- if (currentType === "epub") {
21384
+ if (currentType === 'epub') {
20849
21385
  const result = await loadEpub(payload.source);
20850
21386
  sendState({ outline: result.outline || [] });
20851
21387
  sendResponse(id, true, result);
20852
21388
  return;
20853
21389
  }
20854
21390
 
20855
- throw new Error("Unsupported document type");
21391
+ if (currentType === 'comic') {
21392
+ const result = await loadComic(payload.source, payload.format || 'cbz');
21393
+ sendState();
21394
+ sendResponse(id, true, result);
21395
+ return;
21396
+ }
21397
+
21398
+ throw new Error('Unsupported document type');
20856
21399
  }
20857
21400
 
20858
- if (kind === "go-to-page") {
21401
+ if (kind === 'go-to-page') {
20859
21402
  const page = Math.max(1, payload.page || 1);
20860
- if (currentType === "text") {
21403
+ if (currentType === 'text') {
20861
21404
  currentPage = page;
20862
21405
  renderTextPage(page - 1);
20863
21406
  sendState();
20864
- } else if (currentType === "epub") {
21407
+ } else if (currentType === 'epub') {
20865
21408
  await displayEpubPage(page - 1);
21409
+ } else if (currentType === 'comic') {
21410
+ currentPage = page;
21411
+ const target = comicPages[page - 1];
21412
+ if (target) {
21413
+ await ensureComicPage(page - 1);
21414
+ target.scrollIntoView({ block: 'start' });
21415
+ renderComicPage(page - 1);
21416
+ sendState();
21417
+ }
20866
21418
  }
20867
21419
  sendResponse(id, true, { currentPage });
20868
21420
  return;
20869
21421
  }
20870
21422
 
20871
- if (kind === "set-zoom") {
21423
+ if (kind === 'set-zoom') {
20872
21424
  zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
20873
21425
  applyZoom();
20874
21426
  sendState();
@@ -20876,103 +21428,132 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20876
21428
  return;
20877
21429
  }
20878
21430
 
20879
- if (kind === "set-rotation") {
21431
+ if (kind === 'set-rotation') {
21432
+ rotation = ((payload.rotation || 0) % 360 + 360) % 360;
21433
+ if (currentType === 'comic') applyComicLayout();
20880
21434
  sendResponse(id, true, {});
20881
21435
  return;
20882
21436
  }
20883
21437
 
20884
- if (kind === "get-text-content") {
21438
+ if (kind === 'get-text-content') {
20885
21439
  const items = await getTextContent(payload.pageIndex || 0);
20886
21440
  sendResponse(id, true, items);
20887
21441
  return;
20888
21442
  }
20889
21443
 
20890
- if (kind === "get-page-dimensions") {
20891
- sendResponse(id, true, getPageDimensions());
21444
+ if (kind === 'get-page-dimensions') {
21445
+ if (currentType === 'comic') {
21446
+ sendResponse(
21447
+ id,
21448
+ true,
21449
+ await getComicPageDimensions(payload.pageIndex || 0)
21450
+ );
21451
+ } else {
21452
+ sendResponse(id, true, getPageDimensions());
21453
+ }
21454
+ return;
21455
+ }
21456
+
21457
+ if (kind === 'get-page-preview') {
21458
+ if (currentType !== 'comic') {
21459
+ sendResponse(id, true, null);
21460
+ return;
21461
+ }
21462
+ sendResponse(
21463
+ id,
21464
+ true,
21465
+ await getComicPagePreview(payload.pageIndex || 0)
21466
+ );
20892
21467
  return;
20893
21468
  }
20894
21469
 
20895
- if (kind === "search-text") {
20896
- const results = await searchText(payload.query || "");
21470
+ if (kind === 'search-text') {
21471
+ const results = await searchText(payload.query || '');
20897
21472
  sendResponse(id, true, results);
20898
21473
  return;
20899
21474
  }
20900
21475
 
20901
- if (kind === "select-text") {
21476
+ if (kind === 'select-text') {
20902
21477
  const selection = await selectText(payload.pageIndex || 0);
20903
21478
  sendResponse(id, true, selection);
20904
21479
  return;
20905
21480
  }
20906
21481
 
20907
- if (kind === "get-outline") {
21482
+ if (kind === 'get-outline') {
20908
21483
  const outline = await buildOutline();
20909
21484
  sendResponse(id, true, outline);
20910
21485
  return;
20911
21486
  }
20912
21487
 
20913
- if (kind === "get-page-index") {
21488
+ if (kind === 'get-page-index') {
20914
21489
  const index = getPageIndex(payload.dest);
20915
21490
  sendResponse(id, true, index);
20916
21491
  return;
20917
21492
  }
20918
21493
 
20919
- if (kind === "destroy") {
21494
+ if (kind === 'destroy') {
20920
21495
  clearViewer();
20921
21496
  sendResponse(id, true, {});
20922
21497
  return;
20923
21498
  }
20924
21499
 
20925
- sendResponse(id, false, null, "Unknown command");
21500
+ sendResponse(id, false, null, 'Unknown command');
20926
21501
  } catch (err) {
20927
- sendResponse(
20928
- id,
20929
- false,
20930
- null,
20931
- err && err.message ? err.message : String(err)
20932
- );
21502
+ sendResponse(id, false, null, err && err.message ? err.message : String(err));
20933
21503
  }
20934
21504
  };
20935
21505
 
20936
21506
  const onMessage = (event) => {
20937
21507
  let message = null;
20938
- const raw = event && typeof event === "object" ? event.data : null;
21508
+ const raw = event && typeof event === 'object' ? event.data : null;
20939
21509
 
20940
- if (raw && typeof raw === "object" && raw.kind && raw.id) {
21510
+ if (raw && typeof raw === 'object' && (raw.kind || raw.type) && raw.id) {
20941
21511
  message = raw;
20942
- } else if (typeof raw === "string") {
20943
- if (raw.startsWith("setImmediate$")) {
20944
- return;
20945
- }
21512
+ } else if (typeof raw === 'string') {
21513
+ if (raw.startsWith('setImmediate$')) return;
20946
21514
  const trimmed = raw.trim();
20947
- if (
20948
- !trimmed ||
20949
- (!trimmed.startsWith("{") && !trimmed.startsWith("["))
20950
- ) {
21515
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
20951
21516
  return;
20952
21517
  }
20953
21518
  try {
20954
21519
  message = JSON.parse(trimmed);
20955
21520
  } catch (err) {
20956
- sendEvent("RUNTIME_ERROR", {
20957
- message: "Failed to parse message",
20958
- context: "runtime.onMessage",
20959
- stack: err && err.stack ? err.stack : null,
20960
- preview: trimmed.slice(0, 200),
20961
- });
20962
21521
  return;
20963
21522
  }
20964
21523
  } else {
20965
21524
  return;
20966
21525
  }
20967
21526
 
20968
- if (!message || !message.kind || message.id == null) return;
20969
- handleCommand(message);
21527
+ if (!message || message.id == null) return;
21528
+
21529
+ if (message.type === 'asset-response') {
21530
+ const pending = runtimeAssetRequests.get(message.id);
21531
+ if (!pending) return;
21532
+ runtimeAssetRequests.delete(message.id);
21533
+ clearTimeout(pending.timeout);
21534
+ if (message.ok) pending.resolve(message.data);
21535
+ else pending.reject(new Error(message.error || 'Falha ao carregar asset CBR.'));
21536
+ return;
21537
+ }
21538
+
21539
+ if (message.type === 'file-chunk-response') {
21540
+ const pending = fileChunkRequests.get(message.id);
21541
+ if (!pending) return;
21542
+ fileChunkRequests.delete(message.id);
21543
+ clearTimeout(pending.timeout);
21544
+ if (message.ok) pending.resolve(message);
21545
+ else pending.reject(new Error(message.error || 'Falha ao ler arquivo local.'));
21546
+ return;
21547
+ }
21548
+
21549
+ if (!message.kind) return;
21550
+ handleCommand(message);
20970
21551
  };
20971
21552
 
20972
- window.addEventListener("message", onMessage);
20973
- document.addEventListener("message", onMessage);
21553
+ window.addEventListener('message', onMessage);
21554
+ document.addEventListener('message', onMessage);
20974
21555
 
20975
- sendMessage({ type: "ready" });
21556
+ sendMessage({ type: 'ready' });
20976
21557
  })();
20977
21558
  </script>
20978
21559
  </body>