Posts engine (SQLite + auto-publisher via poller), public feed with clickable tags, reader, admin panel, submit/search/queue tools. BFF proxy to pipeline API. Clean dark design. Docker-ready.
42 lines
1.1 KiB
TypeScript
42 lines
1.1 KiB
TypeScript
import JSZip from "jszip"
|
|
|
|
const imageExts = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"])
|
|
|
|
export async function loadPagesFromCbz(
|
|
url: string,
|
|
signal?: AbortSignal,
|
|
): Promise<{ pages: string[]; pageCount: number }> {
|
|
const response = await fetch(url, { signal })
|
|
if (!response.ok) throw new Error(`Failed to fetch CBZ: ${response.status}`)
|
|
|
|
const blob = await response.blob()
|
|
const zip = await JSZip.loadAsync(blob)
|
|
|
|
const imageEntries = Object.entries(zip.files)
|
|
.filter(([name, file]) => {
|
|
const ext = name.toLowerCase().slice(name.lastIndexOf("."))
|
|
return !file.dir && imageExts.has(ext)
|
|
})
|
|
.sort(([a], [b]) => {
|
|
const numA = parseInt(a.match(/(\d+)/)?.[1] || "0", 10)
|
|
const numB = parseInt(b.match(/(\d+)/)?.[1] || "0", 10)
|
|
return numA - numB
|
|
})
|
|
|
|
const pageCount = imageEntries.length
|
|
const pages: string[] = []
|
|
|
|
for (const [, file] of imageEntries) {
|
|
const blob = await file.async("blob")
|
|
pages.push(URL.createObjectURL(blob))
|
|
}
|
|
|
|
return { pages, pageCount }
|
|
}
|
|
|
|
export function revokePageUrls(urls: string[]) {
|
|
for (const url of urls) {
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
}
|