// Transport-only lookup. Review eligibility continues to belong to the story // and release manifests; an HTTPS URL never confers audio approval. const manifest = require('./cosMediaManifest') const paths = Object.create(null) const urls = Object.create(null) const SHARE_ASSET_ID = 'image.tang.share-preview' for (const entry of manifest.entries) { paths[entry.sourcePath] = entry urls[entry.url] = entry } function entryFor(value) { if (typeof value !== 'string') return null if (urls[value]) return urls[value] let relative = value.replace(/^\//, '') if (relative.startsWith(`${manifest.namespace}/`)) { relative = relative.slice(manifest.namespace.length + 1) } return paths[relative] || null } function resolve(value) { const entry = entryFor(value) return entry ? entry.url : '' } function verifiedUrl(value, sha256, kind) { const entry = urls[value] return entry && entry.sha256 === sha256 && entry.kind === kind ? entry.url : '' } function mapMedia(value) { if (typeof value === 'string') { if (/^\/(?:[a-z0-9-]+\/)?(?:assets|package-[a-z0-9-]+)\/.*\.(?:jpe?g|png|webp|gif|svg|avif|mp3|wav|aac|m4a|ogg|mp4|webm|mov)$/i.test(value)) { // Unregistered declarations remain text-only; never guess an object URL. return resolve(value) } return value } if (Array.isArray(value)) return value.map(mapMedia) if (!value || typeof value !== 'object') return value const mapped = {} Object.keys(value).forEach(key => { mapped[key] = mapMedia(value[key]) }) return mapped } function beginComicImage(page, pageData) { const previous = page._cosComicImage if (!previous || previous.pageId !== pageData.currentPageId) { page._cosComicImage = { pageId: pageData.currentPageId, generation: (previous ? previous.generation : 0) + 1, failed: Object.create(null), lastPatch: null, } } const state = page._cosComicImage pageData.comicImageGeneration = state.generation // Ordinary reader interactions re-render this page. They must not revive a // URL that already failed or replace terminal text fallback with an image. if (state.lastPatch) Object.assign(pageData, state.lastPatch) } function isCurrentComicImageEvent(page, event) { const state = page._cosComicImage const dataset = event && event.currentTarget && event.currentTarget.dataset return Boolean(state && dataset && !page.__tangDead && !page.__tangOriginalUnloaded && page.__tangVisible !== false && page.data.comicImageSrc && state.pageId === page.data.currentPageId && dataset.cosPageId === state.pageId && Number(dataset.cosImageGeneration) === state.generation && dataset.cosImageSrc === page.data.comicImageSrc) } function recordComicImageError(page, event) { if (!isCurrentComicImageEvent(page, event)) return false const state = page._cosComicImage const src = page.data.comicImageSrc if (state.failed[src]) return false state.failed[src] = true return true } function canUseComicFallback(page, src) { const state = page._cosComicImage const entry = entryFor(src) return Boolean(state && entry && entry.kind === 'image' && !state.failed[src]) } function applyComicImageFallback(page, patch) { if (page._cosComicImage) page._cosComicImage.lastPatch = { ...patch } page.setData(patch) } function prepareSharePreview(page, assetManager) { if (page._sharePreviewPromise) return page._sharePreviewPromise const generation = page.__tangShowGeneration const alive = () => !page.__tangDead && !page.__tangOriginalUnloaded && page.__tangVisible !== false && page.__tangShowGeneration === generation // Resolve again for each share; the manager rehashes cached bytes before use. const promise = Promise.resolve().then(() => assetManager.resolve(SHARE_ASSET_ID)) .then(result => { if (!alive()) return '' const localPath = result && result.available && result.uri || '' page.setData({ sharePreviewLocalPath: localPath }) return localPath }) .catch(() => { if (alive()) page.setData({ sharePreviewLocalPath: '' }) return '' }) .finally(() => { if (page._sharePreviewPromise === promise) page._sharePreviewPromise = null }) page._sharePreviewPromise = promise return promise } module.exports = { SHARE_ASSET_ID, entryFor, resolve, verifiedUrl, mapMedia, prepareSharePreview, beginComicImage, isCurrentComicImageEvent, recordComicImageError, canUseComicFallback, applyComicImageFallback, }