@papyrus-sdk/ui-react-native 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +990 -267
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1032 -308
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/runtime/comicRuntime.js +107 -0
- package/runtime/comicRuntime.test.ts +90 -0
- package/runtime/index.html +829 -248
- package/runtime/runtime.js +744 -3
- package/runtime/syncComicRuntime.mjs +41 -0
package/runtime/runtime.js
CHANGED
|
@@ -1,3 +1,113 @@
|
|
|
1
|
+
/* @papyrus-comic-runtime:start */
|
|
2
|
+
(function (root, factory) {
|
|
3
|
+
if (typeof module === "object" && module.exports) {
|
|
4
|
+
module.exports = factory();
|
|
5
|
+
} else {
|
|
6
|
+
root.PapyrusComicRuntime = Object.assign(
|
|
7
|
+
factory(),
|
|
8
|
+
root.PapyrusComicRuntime || {}
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
})(typeof self === "object" ? self : this, function () {
|
|
12
|
+
var IMAGE_EXTENSIONS = /\.(?:gif|jpe?g|png|svg|webp)$/i;
|
|
13
|
+
|
|
14
|
+
function isComicImageName(name) {
|
|
15
|
+
return typeof name === "string" && IMAGE_EXTENSIONS.test(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function naturalCompare(left, right) {
|
|
19
|
+
var leftParts = String(left).split(/(\d+)/);
|
|
20
|
+
var rightParts = String(right).split(/(\d+)/);
|
|
21
|
+
var length = Math.max(leftParts.length, rightParts.length);
|
|
22
|
+
|
|
23
|
+
for (var index = 0; index < length; index += 1) {
|
|
24
|
+
var leftPart = leftParts[index] || "";
|
|
25
|
+
var rightPart = rightParts[index] || "";
|
|
26
|
+
if (leftPart === rightPart) continue;
|
|
27
|
+
|
|
28
|
+
var leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
|
|
29
|
+
var rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
|
|
30
|
+
if (leftNumber !== null && rightNumber !== null) {
|
|
31
|
+
return leftNumber - rightNumber;
|
|
32
|
+
}
|
|
33
|
+
return leftPart.localeCompare(rightPart, undefined, {
|
|
34
|
+
numeric: true,
|
|
35
|
+
sensitivity: "base",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sortComicPageNames(names) {
|
|
43
|
+
return names.slice().sort(naturalCompare);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function patchCbrWorkerSource(workerSource, wasmUrl) {
|
|
47
|
+
return workerSource.replace(
|
|
48
|
+
/new URL\("libarchive\.wasm",import\.meta\.url\)\.href/g,
|
|
49
|
+
JSON.stringify(wasmUrl)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getComicPreviewSize(width, height, maxWidth, maxHeight) {
|
|
54
|
+
var safeWidth = Math.max(1, Number(width) || 1);
|
|
55
|
+
var safeHeight = Math.max(1, Number(height) || 1);
|
|
56
|
+
var widthLimit = maxWidth || 240;
|
|
57
|
+
var heightLimit = maxHeight || 360;
|
|
58
|
+
var scale = Math.min(1, widthLimit / safeWidth, heightLimit / safeHeight);
|
|
59
|
+
return {
|
|
60
|
+
width: Math.max(1, Math.round(safeWidth * scale)),
|
|
61
|
+
height: Math.max(1, Math.round(safeHeight * scale)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getComicPageAspectRatio(width, height) {
|
|
66
|
+
var safeWidth = Math.max(1, Number(width) || 1);
|
|
67
|
+
var safeHeight = Math.max(1, Number(height) || 1);
|
|
68
|
+
return safeWidth + " / " + safeHeight;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getProtectedComicPageIndexes(
|
|
72
|
+
currentPage,
|
|
73
|
+
visiblePageIndexes,
|
|
74
|
+
loadingPageIndexes
|
|
75
|
+
) {
|
|
76
|
+
var protectedIndexes = new Set([
|
|
77
|
+
currentPage - 1,
|
|
78
|
+
currentPage,
|
|
79
|
+
currentPage - 2,
|
|
80
|
+
]);
|
|
81
|
+
(visiblePageIndexes || []).forEach(function (pageIndex) {
|
|
82
|
+
protectedIndexes.add(pageIndex);
|
|
83
|
+
});
|
|
84
|
+
(loadingPageIndexes || []).forEach(function (pageIndex) {
|
|
85
|
+
protectedIndexes.add(pageIndex);
|
|
86
|
+
});
|
|
87
|
+
return protectedIndexes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isCurrentComicPageLoad(
|
|
91
|
+
loadGeneration,
|
|
92
|
+
currentGeneration,
|
|
93
|
+
entry,
|
|
94
|
+
currentEntry
|
|
95
|
+
) {
|
|
96
|
+
return loadGeneration === currentGeneration && entry === currentEntry;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
isComicImageName: isComicImageName,
|
|
101
|
+
sortComicPageNames: sortComicPageNames,
|
|
102
|
+
patchCbrWorkerSource: patchCbrWorkerSource,
|
|
103
|
+
getComicPreviewSize: getComicPreviewSize,
|
|
104
|
+
getComicPageAspectRatio: getComicPageAspectRatio,
|
|
105
|
+
getProtectedComicPageIndexes: getProtectedComicPageIndexes,
|
|
106
|
+
isCurrentComicPageLoad: isCurrentComicPageLoad,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
/* @papyrus-comic-runtime:end */
|
|
110
|
+
|
|
1
111
|
(function () {
|
|
2
112
|
const viewer = document.getElementById('viewer');
|
|
3
113
|
const DEFAULT_FONT_SIZE = 16;
|
|
@@ -12,6 +122,25 @@
|
|
|
12
122
|
let currentPage = 1;
|
|
13
123
|
let pageCount = 0;
|
|
14
124
|
let zoom = 1.0;
|
|
125
|
+
let rotation = 0;
|
|
126
|
+
let comicArchive = null;
|
|
127
|
+
let comicEntries = [];
|
|
128
|
+
let comicContainer = null;
|
|
129
|
+
let comicPages = [];
|
|
130
|
+
let comicObserver = null;
|
|
131
|
+
let comicScrollFrame = null;
|
|
132
|
+
const comicVisiblePages = new Set();
|
|
133
|
+
const comicObjectUrls = new Map();
|
|
134
|
+
const comicLoading = new Map();
|
|
135
|
+
const comicDimensions = new Map();
|
|
136
|
+
const comicPreviewCache = new Map();
|
|
137
|
+
const comicPreviewLoading = new Map();
|
|
138
|
+
const COMIC_CACHE_LIMIT = 16;
|
|
139
|
+
const COMIC_PREVIEW_CACHE_LIMIT = 8;
|
|
140
|
+
const comicHelpers = window.PapyrusComicRuntime || {};
|
|
141
|
+
const runtimeConfig = window.__PAPYRUS_RUNTIME_CONFIG__ || {};
|
|
142
|
+
let comicDispose = null;
|
|
143
|
+
let comicGeneration = 0;
|
|
15
144
|
|
|
16
145
|
const sendMessage = (payload) => {
|
|
17
146
|
if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
|
|
@@ -27,6 +156,80 @@
|
|
|
27
156
|
sendMessage({ type: 'response', id, ok, data, error });
|
|
28
157
|
};
|
|
29
158
|
|
|
159
|
+
const runtimeAssetRequests = new Map();
|
|
160
|
+
const fileChunkRequests = new Map();
|
|
161
|
+
|
|
162
|
+
const requestRuntimeAsset = (url, encoding) =>
|
|
163
|
+
new Promise((resolve, reject) => {
|
|
164
|
+
const id = `runtime-asset-${Date.now()}-${Math.random()}`;
|
|
165
|
+
const timeout = setTimeout(() => {
|
|
166
|
+
runtimeAssetRequests.delete(id);
|
|
167
|
+
reject(new Error(`Tempo esgotado ao carregar asset CBR: ${url}`));
|
|
168
|
+
}, 15000);
|
|
169
|
+
runtimeAssetRequests.set(id, {resolve, reject, timeout});
|
|
170
|
+
sendMessage({type: 'asset-request', id, url, encoding});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const encodeArrayBufferAsDataUrl = async (buffer) => {
|
|
174
|
+
const bytes = new Uint8Array(buffer);
|
|
175
|
+
let binary = '';
|
|
176
|
+
const chunkSize = 0x8000;
|
|
177
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
178
|
+
binary += String.fromCharCode(
|
|
179
|
+
...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return `data:application/wasm;base64,${btoa(binary)}`;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const fetchRuntimeAsset = async (url, encoding) => {
|
|
186
|
+
try {
|
|
187
|
+
const response = await fetch(url);
|
|
188
|
+
if (!response.ok) throw new Error(`status ${response.status}`);
|
|
189
|
+
if (encoding === 'text') return await response.text();
|
|
190
|
+
return await encodeArrayBufferAsDataUrl(await response.arrayBuffer());
|
|
191
|
+
} catch {
|
|
192
|
+
return await requestRuntimeAsset(url, encoding);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const requestLocalFileChunk = (uri, offset, length) =>
|
|
197
|
+
new Promise((resolve, reject) => {
|
|
198
|
+
const id = `file-chunk-${Date.now()}-${Math.random()}`;
|
|
199
|
+
const timeout = setTimeout(() => {
|
|
200
|
+
fileChunkRequests.delete(id);
|
|
201
|
+
reject(new Error(`Tempo esgotado ao ler arquivo local: ${uri}`));
|
|
202
|
+
}, 30000);
|
|
203
|
+
fileChunkRequests.set(id, {resolve, reject, timeout});
|
|
204
|
+
sendMessage({type: 'file-chunk-request', id, uri, offset, length});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const readLocalFile = async (uri) => {
|
|
208
|
+
const chunkSize = 1024 * 1024;
|
|
209
|
+
const chunks = [];
|
|
210
|
+
let offset = 0;
|
|
211
|
+
let totalLength = 0;
|
|
212
|
+
while (true) {
|
|
213
|
+
const result = await requestLocalFileChunk(uri, offset, chunkSize);
|
|
214
|
+
const bytes = decodeBase64(result.data || '');
|
|
215
|
+
if (bytes.length === 0 && !result.done) {
|
|
216
|
+
throw new Error('Leitura nativa retornou um chunk vazio.');
|
|
217
|
+
}
|
|
218
|
+
chunks.push(bytes);
|
|
219
|
+
totalLength += bytes.length;
|
|
220
|
+
offset += bytes.length;
|
|
221
|
+
if (result.done) break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const output = new Uint8Array(totalLength);
|
|
225
|
+
let outputOffset = 0;
|
|
226
|
+
chunks.forEach((chunk) => {
|
|
227
|
+
output.set(chunk, outputOffset);
|
|
228
|
+
outputOffset += chunk.length;
|
|
229
|
+
});
|
|
230
|
+
return output.buffer;
|
|
231
|
+
};
|
|
232
|
+
|
|
30
233
|
const sendState = (extra) => {
|
|
31
234
|
sendMessage({
|
|
32
235
|
type: 'state',
|
|
@@ -43,7 +246,57 @@
|
|
|
43
246
|
sendMessage({ type: 'event', name, payload });
|
|
44
247
|
};
|
|
45
248
|
|
|
249
|
+
const reportError = (error, context) => {
|
|
250
|
+
const message = error && error.message ? error.message : String(error);
|
|
251
|
+
const stack = error && error.stack ? error.stack : null;
|
|
252
|
+
sendEvent('RUNTIME_ERROR', { message, context, stack });
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const shouldIgnoreError = (error) => {
|
|
256
|
+
const message = error && error.message ? error.message : String(error || '');
|
|
257
|
+
return message.includes('ResizeObserver loop');
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
window.addEventListener('error', (event) => {
|
|
261
|
+
const error = event.error || event.message;
|
|
262
|
+
if (shouldIgnoreError(error)) return;
|
|
263
|
+
reportError(error, 'window.error');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
267
|
+
if (shouldIgnoreError(event.reason)) return;
|
|
268
|
+
reportError(event.reason, 'unhandledrejection');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const clearComicState = () => {
|
|
272
|
+
comicGeneration += 1;
|
|
273
|
+
if (comicObserver) {
|
|
274
|
+
comicObserver.disconnect();
|
|
275
|
+
comicObserver = null;
|
|
276
|
+
}
|
|
277
|
+
if (comicScrollFrame !== null) {
|
|
278
|
+
cancelAnimationFrame(comicScrollFrame);
|
|
279
|
+
comicScrollFrame = null;
|
|
280
|
+
}
|
|
281
|
+
comicObjectUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
282
|
+
comicObjectUrls.clear();
|
|
283
|
+
comicLoading.clear();
|
|
284
|
+
comicVisiblePages.clear();
|
|
285
|
+
comicDimensions.clear();
|
|
286
|
+
comicPreviewCache.clear();
|
|
287
|
+
comicPreviewLoading.clear();
|
|
288
|
+
if (comicDispose) {
|
|
289
|
+
void comicDispose();
|
|
290
|
+
comicDispose = null;
|
|
291
|
+
}
|
|
292
|
+
comicArchive = null;
|
|
293
|
+
comicEntries = [];
|
|
294
|
+
comicContainer = null;
|
|
295
|
+
comicPages = [];
|
|
296
|
+
};
|
|
297
|
+
|
|
46
298
|
const clearViewer = () => {
|
|
299
|
+
clearComicState();
|
|
47
300
|
while (viewer.firstChild) {
|
|
48
301
|
viewer.removeChild(viewer.firstChild);
|
|
49
302
|
}
|
|
@@ -65,6 +318,431 @@
|
|
|
65
318
|
return new TextDecoder('utf-8').decode(bytes);
|
|
66
319
|
};
|
|
67
320
|
|
|
321
|
+
const sourceToArrayBuffer = async (source) => {
|
|
322
|
+
if (source.kind === 'uri') {
|
|
323
|
+
if (/^(?:file|content):\/\//i.test(source.uri)) {
|
|
324
|
+
return readLocalFile(source.uri);
|
|
325
|
+
}
|
|
326
|
+
const response = await fetch(source.uri);
|
|
327
|
+
if (!response.ok && response.status !== 0) {
|
|
328
|
+
throw new Error(`Falha ao carregar quadrinho (${response.status})`);
|
|
329
|
+
}
|
|
330
|
+
return response.arrayBuffer();
|
|
331
|
+
}
|
|
332
|
+
if (source.kind === 'base64') {
|
|
333
|
+
return decodeBase64(source.data).buffer;
|
|
334
|
+
}
|
|
335
|
+
throw new Error('Fonte de quadrinho inválida.');
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const comicMimeType = (name) => {
|
|
339
|
+
const extension = name.split('.').pop()?.toLowerCase();
|
|
340
|
+
if (extension === 'png') return 'image/png';
|
|
341
|
+
if (extension === 'gif') return 'image/gif';
|
|
342
|
+
if (extension === 'svg') return 'image/svg+xml';
|
|
343
|
+
if (extension === 'webp') return 'image/webp';
|
|
344
|
+
return 'image/jpeg';
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const updateComicCurrentPage = () => {
|
|
348
|
+
if (!comicContainer || comicPages.length === 0) return;
|
|
349
|
+
const viewportTop = comicContainer.scrollTop;
|
|
350
|
+
let closestIndex = currentPage - 1;
|
|
351
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
352
|
+
comicPages.forEach((page, index) => {
|
|
353
|
+
const distance = Math.abs(page.offsetTop - viewportTop);
|
|
354
|
+
if (distance < closestDistance) {
|
|
355
|
+
closestDistance = distance;
|
|
356
|
+
closestIndex = index;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
const nextPage = Math.max(1, Math.min(pageCount, closestIndex + 1));
|
|
360
|
+
if (nextPage !== currentPage) {
|
|
361
|
+
currentPage = nextPage;
|
|
362
|
+
sendState();
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const scheduleComicCurrentPage = () => {
|
|
367
|
+
if (comicScrollFrame !== null) return;
|
|
368
|
+
comicScrollFrame = requestAnimationFrame(() => {
|
|
369
|
+
comicScrollFrame = null;
|
|
370
|
+
updateComicCurrentPage();
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const evictComicUrls = () => {
|
|
375
|
+
if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) return;
|
|
376
|
+
const protectedIndexes = comicHelpers.getProtectedComicPageIndexes(
|
|
377
|
+
currentPage,
|
|
378
|
+
Array.from(comicVisiblePages),
|
|
379
|
+
Array.from(comicLoading.keys())
|
|
380
|
+
);
|
|
381
|
+
for (const [index, url] of comicObjectUrls) {
|
|
382
|
+
if (comicObjectUrls.size <= COMIC_CACHE_LIMIT) break;
|
|
383
|
+
if (protectedIndexes.has(index)) continue;
|
|
384
|
+
URL.revokeObjectURL(url);
|
|
385
|
+
comicObjectUrls.delete(index);
|
|
386
|
+
const image = comicPages[index]?.querySelector('img');
|
|
387
|
+
const page = comicPages[index];
|
|
388
|
+
const dimensions = comicDimensions.get(index);
|
|
389
|
+
const width = dimensions?.width || image?.naturalWidth || 0;
|
|
390
|
+
const height = dimensions?.height || image?.naturalHeight || 0;
|
|
391
|
+
if (page && width > 0 && height > 0) {
|
|
392
|
+
page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
|
|
393
|
+
width,
|
|
394
|
+
height
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (image) image.removeAttribute('src');
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const ensureComicPage = async (pageIndex) => {
|
|
402
|
+
if (comicObjectUrls.has(pageIndex)) {
|
|
403
|
+
const image = comicPages[pageIndex]?.querySelector('img');
|
|
404
|
+
if (image && !image.src) image.src = comicObjectUrls.get(pageIndex);
|
|
405
|
+
return comicObjectUrls.get(pageIndex);
|
|
406
|
+
}
|
|
407
|
+
if (comicLoading.has(pageIndex)) return comicLoading.get(pageIndex);
|
|
408
|
+
|
|
409
|
+
const entry = comicEntries[pageIndex];
|
|
410
|
+
if (!entry) throw new Error(`Página de quadrinho inválida: ${pageIndex}`);
|
|
411
|
+
const loadGeneration = comicGeneration;
|
|
412
|
+
const pending = entry.async('blob').then((blob) => {
|
|
413
|
+
if (
|
|
414
|
+
!comicHelpers.isCurrentComicPageLoad(
|
|
415
|
+
loadGeneration,
|
|
416
|
+
comicGeneration,
|
|
417
|
+
entry,
|
|
418
|
+
comicEntries[pageIndex]
|
|
419
|
+
)
|
|
420
|
+
) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const url = URL.createObjectURL(
|
|
424
|
+
new Blob([blob], { type: comicMimeType(entry.name) })
|
|
425
|
+
);
|
|
426
|
+
comicObjectUrls.set(pageIndex, url);
|
|
427
|
+
const image = comicPages[pageIndex]?.querySelector('img');
|
|
428
|
+
if (image) image.src = url;
|
|
429
|
+
evictComicUrls();
|
|
430
|
+
return url;
|
|
431
|
+
});
|
|
432
|
+
comicLoading.set(pageIndex, pending);
|
|
433
|
+
try {
|
|
434
|
+
return await pending;
|
|
435
|
+
} finally {
|
|
436
|
+
if (comicLoading.get(pageIndex) === pending) {
|
|
437
|
+
comicLoading.delete(pageIndex);
|
|
438
|
+
evictComicUrls();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const renderComicPage = (pageIndex) => {
|
|
444
|
+
const image = comicPages[pageIndex]?.querySelector('img');
|
|
445
|
+
if (!image) return;
|
|
446
|
+
image.style.width = `${Math.max(40, Math.round(100 * zoom))}%`;
|
|
447
|
+
image.style.transform = `rotate(${rotation}deg)`;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const applyComicLayout = () => {
|
|
451
|
+
comicPages.forEach((_, pageIndex) => renderComicPage(pageIndex));
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const loadComic = async (source, format) => {
|
|
455
|
+
clearViewer();
|
|
456
|
+
let entries;
|
|
457
|
+
if (format === 'cbr') {
|
|
458
|
+
const clientUrl = runtimeConfig.cbrClientUrl;
|
|
459
|
+
const workerUrl = runtimeConfig.cbrWorkerUrl;
|
|
460
|
+
const wasmUrl = runtimeConfig.cbrWasmUrl;
|
|
461
|
+
if (!clientUrl || !workerUrl || !wasmUrl) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
'CBR no mobile exige o pacote opcional de runtime libarchive.'
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const [clientSource, workerSource, wasmSource] = await Promise.all([
|
|
468
|
+
fetchRuntimeAsset(clientUrl, 'text'),
|
|
469
|
+
fetchRuntimeAsset(workerUrl, 'text'),
|
|
470
|
+
runtimeConfig.cbrWasmDataUrl ||
|
|
471
|
+
fetchRuntimeAsset(wasmUrl, 'base64'),
|
|
472
|
+
]);
|
|
473
|
+
const patchedWorkerSource = comicHelpers.patchCbrWorkerSource(
|
|
474
|
+
workerSource,
|
|
475
|
+
wasmSource
|
|
476
|
+
);
|
|
477
|
+
const workerObjectUrl = URL.createObjectURL(
|
|
478
|
+
new Blob([patchedWorkerSource], { type: 'text/javascript' })
|
|
479
|
+
);
|
|
480
|
+
const clientObjectUrl = URL.createObjectURL(
|
|
481
|
+
new Blob([clientSource], { type: 'text/javascript' })
|
|
482
|
+
);
|
|
483
|
+
try {
|
|
484
|
+
const archiveModule = await import(clientObjectUrl);
|
|
485
|
+
const createCbrWorker = () => {
|
|
486
|
+
const worker = new Worker(workerObjectUrl, {type: 'module'});
|
|
487
|
+
worker.addEventListener('error', (event) => {
|
|
488
|
+
sendEvent('RUNTIME_ERROR', {
|
|
489
|
+
message: `CBR worker: ${event?.message || 'falha ao executar o worker'}`,
|
|
490
|
+
context: 'comic.worker',
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
worker.addEventListener('messageerror', () => {
|
|
494
|
+
sendEvent('RUNTIME_ERROR', {
|
|
495
|
+
message: 'CBR worker: falha ao transferir mensagem',
|
|
496
|
+
context: 'comic.worker',
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
return worker;
|
|
500
|
+
};
|
|
501
|
+
archiveModule.Archive.init({ getWorker: createCbrWorker });
|
|
502
|
+
const bytes = await sourceToArrayBuffer(source);
|
|
503
|
+
const file = new Blob([bytes], {
|
|
504
|
+
type: 'application/vnd.comicbook-rar',
|
|
505
|
+
});
|
|
506
|
+
const archive = await archiveModule.Archive.open(file);
|
|
507
|
+
const files = await archive.getFilesArray();
|
|
508
|
+
entries = files
|
|
509
|
+
.filter(
|
|
510
|
+
(item) => {
|
|
511
|
+
const name = `${item?.path || ''}${item?.file?.name || ''}`;
|
|
512
|
+
return item?.file && comicHelpers.isComicImageName(name);
|
|
513
|
+
}
|
|
514
|
+
)
|
|
515
|
+
.map((item) => {
|
|
516
|
+
const name = `${item.path || ''}${item.file.name}`;
|
|
517
|
+
return {
|
|
518
|
+
name,
|
|
519
|
+
async: async () => item.file.extract(),
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
comicDispose = async () => {
|
|
523
|
+
await archive.close();
|
|
524
|
+
URL.revokeObjectURL(workerObjectUrl);
|
|
525
|
+
URL.revokeObjectURL(clientObjectUrl);
|
|
526
|
+
};
|
|
527
|
+
} catch (error) {
|
|
528
|
+
URL.revokeObjectURL(workerObjectUrl);
|
|
529
|
+
URL.revokeObjectURL(clientObjectUrl);
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
if (!window.JSZip) {
|
|
534
|
+
throw new Error('O runtime ZIP do leitor mobile não foi carregado.');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const bytes = await sourceToArrayBuffer(source);
|
|
538
|
+
comicArchive = await window.JSZip.loadAsync(bytes);
|
|
539
|
+
entries = Object.keys(comicArchive.files)
|
|
540
|
+
.filter((name) => {
|
|
541
|
+
const entry = comicArchive.files[name];
|
|
542
|
+
return !entry.dir && comicHelpers.isComicImageName(name);
|
|
543
|
+
})
|
|
544
|
+
.map((name) => comicArchive.files[name]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const names = comicHelpers.sortComicPageNames(
|
|
548
|
+
entries.map((entry) => entry.name)
|
|
549
|
+
);
|
|
550
|
+
const entriesByName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
551
|
+
comicEntries = names.map((name) => entriesByName.get(name));
|
|
552
|
+
if (comicEntries.length === 0) {
|
|
553
|
+
if (comicDispose) {
|
|
554
|
+
await comicDispose();
|
|
555
|
+
comicDispose = null;
|
|
556
|
+
}
|
|
557
|
+
throw new Error('O arquivo não contém páginas de imagem.');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
comicContainer = document.createElement('div');
|
|
561
|
+
comicContainer.style.width = '100%';
|
|
562
|
+
comicContainer.style.height = '100%';
|
|
563
|
+
comicContainer.style.overflow = 'auto';
|
|
564
|
+
comicContainer.style.padding = '16px 0 120px';
|
|
565
|
+
comicContainer.style.boxSizing = 'border-box';
|
|
566
|
+
comicContainer.style.background = '#ffffff';
|
|
567
|
+
comicContainer.addEventListener('scroll', scheduleComicCurrentPage, {
|
|
568
|
+
passive: true,
|
|
569
|
+
});
|
|
570
|
+
viewer.style.overflow = 'hidden';
|
|
571
|
+
viewer.appendChild(comicContainer);
|
|
572
|
+
|
|
573
|
+
comicPages = comicEntries.map((entry, pageIndex) => {
|
|
574
|
+
const page = document.createElement('div');
|
|
575
|
+
page.dataset.pageIndex = String(pageIndex);
|
|
576
|
+
page.style.width = '100%';
|
|
577
|
+
page.style.minHeight = '80px';
|
|
578
|
+
page.style.display = 'flex';
|
|
579
|
+
page.style.justifyContent = 'center';
|
|
580
|
+
page.style.alignItems = 'flex-start';
|
|
581
|
+
page.style.padding = '0 12px 20px';
|
|
582
|
+
page.style.boxSizing = 'border-box';
|
|
583
|
+
|
|
584
|
+
const image = document.createElement('img');
|
|
585
|
+
image.alt = `Página ${pageIndex + 1}`;
|
|
586
|
+
image.draggable = false;
|
|
587
|
+
image.decoding = 'async';
|
|
588
|
+
image.loading = 'lazy';
|
|
589
|
+
image.style.display = 'block';
|
|
590
|
+
image.style.maxWidth = 'none';
|
|
591
|
+
image.style.height = 'auto';
|
|
592
|
+
image.style.userSelect = 'none';
|
|
593
|
+
image.addEventListener('load', () => {
|
|
594
|
+
if (image.naturalWidth > 0 && image.naturalHeight > 0) {
|
|
595
|
+
comicDimensions.set(pageIndex, {
|
|
596
|
+
width: image.naturalWidth,
|
|
597
|
+
height: image.naturalHeight,
|
|
598
|
+
});
|
|
599
|
+
page.style.aspectRatio = comicHelpers.getComicPageAspectRatio(
|
|
600
|
+
image.naturalWidth,
|
|
601
|
+
image.naturalHeight
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
page.appendChild(image);
|
|
606
|
+
comicContainer.appendChild(page);
|
|
607
|
+
return page;
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
if (typeof IntersectionObserver === 'function') {
|
|
611
|
+
comicObserver = new IntersectionObserver(
|
|
612
|
+
(observations) => {
|
|
613
|
+
observations.forEach((observation) => {
|
|
614
|
+
const pageIndex = Number(observation.target.dataset.pageIndex);
|
|
615
|
+
if (!observation.isIntersecting) {
|
|
616
|
+
comicVisiblePages.delete(pageIndex);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
comicVisiblePages.add(pageIndex);
|
|
620
|
+
void ensureComicPage(pageIndex).catch((error) =>
|
|
621
|
+
sendEvent('RUNTIME_ERROR', {
|
|
622
|
+
message: error?.message || String(error),
|
|
623
|
+
context: 'comic.page',
|
|
624
|
+
})
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
scheduleComicCurrentPage();
|
|
628
|
+
},
|
|
629
|
+
{ root: comicContainer, rootMargin: '600px 0px' }
|
|
630
|
+
);
|
|
631
|
+
comicPages.forEach((page) => comicObserver.observe(page));
|
|
632
|
+
} else {
|
|
633
|
+
for (let pageIndex = 0; pageIndex < Math.min(3, comicPages.length); pageIndex += 1) {
|
|
634
|
+
void ensureComicPage(pageIndex);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
pageCount = comicPages.length;
|
|
638
|
+
currentPage = 1;
|
|
639
|
+
await ensureComicPage(0);
|
|
640
|
+
applyComicLayout();
|
|
641
|
+
return { pageCount };
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const readComicBlobAsDataUrl = (blob, mimeType) =>
|
|
645
|
+
new Promise((resolve, reject) => {
|
|
646
|
+
const reader = new FileReader();
|
|
647
|
+
reader.onerror = () => reject(new Error('Falha ao gerar preview.'));
|
|
648
|
+
reader.onload = () => resolve(reader.result);
|
|
649
|
+
reader.readAsDataURL(new Blob([blob], { type: mimeType }));
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const createComicPagePreview = async (pageIndex) => {
|
|
653
|
+
const entry = comicEntries[pageIndex];
|
|
654
|
+
if (!entry) return null;
|
|
655
|
+
const mimeType = comicMimeType(entry.name);
|
|
656
|
+
const sourceUrl = await ensureComicPage(pageIndex);
|
|
657
|
+
if (!sourceUrl) return null;
|
|
658
|
+
try {
|
|
659
|
+
const image = new Image();
|
|
660
|
+
image.decoding = 'async';
|
|
661
|
+
await new Promise((resolve, reject) => {
|
|
662
|
+
image.onload = resolve;
|
|
663
|
+
image.onerror = () => reject(new Error('Falha ao decodificar preview.'));
|
|
664
|
+
image.src = sourceUrl;
|
|
665
|
+
});
|
|
666
|
+
const size = comicHelpers.getComicPreviewSize(
|
|
667
|
+
image.naturalWidth || image.width,
|
|
668
|
+
image.naturalHeight || image.height
|
|
669
|
+
);
|
|
670
|
+
const canvas = document.createElement('canvas');
|
|
671
|
+
canvas.width = size.width;
|
|
672
|
+
canvas.height = size.height;
|
|
673
|
+
const context = canvas.getContext('2d');
|
|
674
|
+
if (!context) throw new Error('Canvas indisponível para preview.');
|
|
675
|
+
context.fillStyle = '#ffffff';
|
|
676
|
+
context.fillRect(0, 0, size.width, size.height);
|
|
677
|
+
context.drawImage(image, 0, 0, size.width, size.height);
|
|
678
|
+
return canvas.toDataURL('image/jpeg', 0.78);
|
|
679
|
+
} catch {
|
|
680
|
+
try {
|
|
681
|
+
const response = await fetch(sourceUrl);
|
|
682
|
+
const blob = await response.blob();
|
|
683
|
+
return await readComicBlobAsDataUrl(blob, mimeType);
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const getComicPagePreview = async (pageIndex) => {
|
|
691
|
+
const entry = comicEntries[pageIndex];
|
|
692
|
+
if (!entry) return null;
|
|
693
|
+
if (comicPreviewCache.has(pageIndex)) {
|
|
694
|
+
return comicPreviewCache.get(pageIndex);
|
|
695
|
+
}
|
|
696
|
+
if (comicPreviewLoading.has(pageIndex)) {
|
|
697
|
+
return comicPreviewLoading.get(pageIndex);
|
|
698
|
+
}
|
|
699
|
+
const loadGeneration = comicGeneration;
|
|
700
|
+
const pending = createComicPagePreview(pageIndex).then((preview) => {
|
|
701
|
+
if (
|
|
702
|
+
!comicHelpers.isCurrentComicPageLoad(
|
|
703
|
+
loadGeneration,
|
|
704
|
+
comicGeneration,
|
|
705
|
+
entry,
|
|
706
|
+
comicEntries[pageIndex]
|
|
707
|
+
)
|
|
708
|
+
) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
comicPreviewCache.set(pageIndex, preview);
|
|
712
|
+
while (comicPreviewCache.size > COMIC_PREVIEW_CACHE_LIMIT) {
|
|
713
|
+
comicPreviewCache.delete(comicPreviewCache.keys().next().value);
|
|
714
|
+
}
|
|
715
|
+
return preview;
|
|
716
|
+
});
|
|
717
|
+
comicPreviewLoading.set(pageIndex, pending);
|
|
718
|
+
try {
|
|
719
|
+
return await pending;
|
|
720
|
+
} finally {
|
|
721
|
+
if (comicPreviewLoading.get(pageIndex) === pending) {
|
|
722
|
+
comicPreviewLoading.delete(pageIndex);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const getComicPageDimensions = async (pageIndex) => {
|
|
728
|
+
const known = comicDimensions.get(pageIndex);
|
|
729
|
+
if (known) return known;
|
|
730
|
+
await ensureComicPage(pageIndex);
|
|
731
|
+
const image = comicPages[pageIndex]?.querySelector('img');
|
|
732
|
+
if (image && !image.complete) {
|
|
733
|
+
await new Promise((resolve) => {
|
|
734
|
+
image.addEventListener('load', resolve, { once: true });
|
|
735
|
+
image.addEventListener('error', resolve, { once: true });
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return (
|
|
739
|
+
comicDimensions.get(pageIndex) || {
|
|
740
|
+
width: image?.naturalWidth || 0,
|
|
741
|
+
height: image?.naturalHeight || 0,
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
};
|
|
745
|
+
|
|
68
746
|
const normalizeHref = (href) => {
|
|
69
747
|
if (!href) return '';
|
|
70
748
|
return href.split('#')[0];
|
|
@@ -316,6 +994,8 @@
|
|
|
316
994
|
applyTextZoom();
|
|
317
995
|
} else if (currentType === 'epub') {
|
|
318
996
|
applyEpubZoom();
|
|
997
|
+
} else if (currentType === 'comic') {
|
|
998
|
+
applyComicLayout();
|
|
319
999
|
}
|
|
320
1000
|
};
|
|
321
1001
|
|
|
@@ -341,6 +1021,13 @@
|
|
|
341
1021
|
return;
|
|
342
1022
|
}
|
|
343
1023
|
|
|
1024
|
+
if (currentType === 'comic') {
|
|
1025
|
+
const result = await loadComic(payload.source, payload.format || 'cbz');
|
|
1026
|
+
sendState();
|
|
1027
|
+
sendResponse(id, true, result);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
344
1031
|
throw new Error('Unsupported document type');
|
|
345
1032
|
}
|
|
346
1033
|
|
|
@@ -352,6 +1039,15 @@
|
|
|
352
1039
|
sendState();
|
|
353
1040
|
} else if (currentType === 'epub') {
|
|
354
1041
|
await displayEpubPage(page - 1);
|
|
1042
|
+
} else if (currentType === 'comic') {
|
|
1043
|
+
currentPage = page;
|
|
1044
|
+
const target = comicPages[page - 1];
|
|
1045
|
+
if (target) {
|
|
1046
|
+
await ensureComicPage(page - 1);
|
|
1047
|
+
target.scrollIntoView({ block: 'start' });
|
|
1048
|
+
renderComicPage(page - 1);
|
|
1049
|
+
sendState();
|
|
1050
|
+
}
|
|
355
1051
|
}
|
|
356
1052
|
sendResponse(id, true, { currentPage });
|
|
357
1053
|
return;
|
|
@@ -366,6 +1062,8 @@
|
|
|
366
1062
|
}
|
|
367
1063
|
|
|
368
1064
|
if (kind === 'set-rotation') {
|
|
1065
|
+
rotation = ((payload.rotation || 0) % 360 + 360) % 360;
|
|
1066
|
+
if (currentType === 'comic') applyComicLayout();
|
|
369
1067
|
sendResponse(id, true, {});
|
|
370
1068
|
return;
|
|
371
1069
|
}
|
|
@@ -377,7 +1075,28 @@
|
|
|
377
1075
|
}
|
|
378
1076
|
|
|
379
1077
|
if (kind === 'get-page-dimensions') {
|
|
380
|
-
|
|
1078
|
+
if (currentType === 'comic') {
|
|
1079
|
+
sendResponse(
|
|
1080
|
+
id,
|
|
1081
|
+
true,
|
|
1082
|
+
await getComicPageDimensions(payload.pageIndex || 0)
|
|
1083
|
+
);
|
|
1084
|
+
} else {
|
|
1085
|
+
sendResponse(id, true, getPageDimensions());
|
|
1086
|
+
}
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (kind === 'get-page-preview') {
|
|
1091
|
+
if (currentType !== 'comic') {
|
|
1092
|
+
sendResponse(id, true, null);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
sendResponse(
|
|
1096
|
+
id,
|
|
1097
|
+
true,
|
|
1098
|
+
await getComicPagePreview(payload.pageIndex || 0)
|
|
1099
|
+
);
|
|
381
1100
|
return;
|
|
382
1101
|
}
|
|
383
1102
|
|
|
@@ -421,7 +1140,7 @@
|
|
|
421
1140
|
let message = null;
|
|
422
1141
|
const raw = event && typeof event === 'object' ? event.data : null;
|
|
423
1142
|
|
|
424
|
-
if (raw && typeof raw === 'object' && raw.kind && raw.id) {
|
|
1143
|
+
if (raw && typeof raw === 'object' && (raw.kind || raw.type) && raw.id) {
|
|
425
1144
|
message = raw;
|
|
426
1145
|
} else if (typeof raw === 'string') {
|
|
427
1146
|
if (raw.startsWith('setImmediate$')) return;
|
|
@@ -438,7 +1157,29 @@
|
|
|
438
1157
|
return;
|
|
439
1158
|
}
|
|
440
1159
|
|
|
441
|
-
if (!message ||
|
|
1160
|
+
if (!message || message.id == null) return;
|
|
1161
|
+
|
|
1162
|
+
if (message.type === 'asset-response') {
|
|
1163
|
+
const pending = runtimeAssetRequests.get(message.id);
|
|
1164
|
+
if (!pending) return;
|
|
1165
|
+
runtimeAssetRequests.delete(message.id);
|
|
1166
|
+
clearTimeout(pending.timeout);
|
|
1167
|
+
if (message.ok) pending.resolve(message.data);
|
|
1168
|
+
else pending.reject(new Error(message.error || 'Falha ao carregar asset CBR.'));
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (message.type === 'file-chunk-response') {
|
|
1173
|
+
const pending = fileChunkRequests.get(message.id);
|
|
1174
|
+
if (!pending) return;
|
|
1175
|
+
fileChunkRequests.delete(message.id);
|
|
1176
|
+
clearTimeout(pending.timeout);
|
|
1177
|
+
if (message.ok) pending.resolve(message);
|
|
1178
|
+
else pending.reject(new Error(message.error || 'Falha ao ler arquivo local.'));
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (!message.kind) return;
|
|
442
1183
|
handleCommand(message);
|
|
443
1184
|
};
|
|
444
1185
|
|