feat(gallery): download scene gallery as zip
Signed-off-by: baizhi958216 <1475289190@qq.com>
This commit is contained in:
+17
-84
@@ -15,6 +15,11 @@ import type {
|
||||
Orientation,
|
||||
SceneExit,
|
||||
} from "@infiplot/types";
|
||||
import {
|
||||
downloadImagesIndividually,
|
||||
downloadImagesAsZip,
|
||||
inferImageExtension,
|
||||
} from "@/lib/imageZipDownload";
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Gallery — an offline-only replay of a played session. Entered from
|
||||
@@ -123,72 +128,6 @@ function pickedChoiceIdAt(
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Download a batch of image URLs as separate browser downloads.
|
||||
// Runware CDN sends Access-Control-Allow-Origin (the annotate flow already
|
||||
// relies on this) so fetch().blob() works cross-origin without a proxy.
|
||||
//
|
||||
// Each fetch has its own AbortController + per-file timeout — without that
|
||||
// a single slow/hung CDN response strands the whole loop, the caller's busy
|
||||
// flag never clears, and the button looks "stuck" (the original "下载完按钮就没了"
|
||||
// report). Fetches run in a small concurrency pool to keep total time
|
||||
// reasonable for ~10-30 portraits; the actual <a download> clicks remain
|
||||
// serial with a small gap so Chrome's "allow multiple downloads" prompt
|
||||
// fires once instead of being coalesced or dropped.
|
||||
async function downloadImages(
|
||||
files: { url: string; name: string }[],
|
||||
): Promise<void> {
|
||||
const CONCURRENT_FETCH = 4;
|
||||
const FETCH_TIMEOUT_MS = 20_000;
|
||||
|
||||
async function fetchOne(
|
||||
file: { url: string; name: string },
|
||||
): Promise<{ blobUrl: string; name: string } | null> {
|
||||
const { url, name } = file;
|
||||
if (!url) return null;
|
||||
if (url.startsWith("data:")) return { blobUrl: url, name };
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
const r = await fetch(url, { mode: "cors", signal: ctrl.signal });
|
||||
if (!r.ok) return null;
|
||||
const blob = await r.blob();
|
||||
return { blobUrl: URL.createObjectURL(blob), name };
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
const queue = [...files];
|
||||
const ready: ({ blobUrl: string; name: string } | null)[] = [];
|
||||
await Promise.all(
|
||||
Array.from({ length: CONCURRENT_FETCH }, async () => {
|
||||
while (queue.length > 0) {
|
||||
const f = queue.shift();
|
||||
if (!f) break;
|
||||
ready.push(await fetchOne(f));
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
for (const item of ready) {
|
||||
if (!item) continue;
|
||||
const { blobUrl, name } = item;
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
a.download = name;
|
||||
a.rel = "noopener";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
if (blobUrl.startsWith("blob:")) {
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 1500);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Dialogue panel — full beat trail of the current scene
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
@@ -892,13 +831,6 @@ function GalleryInner() {
|
||||
if (!doc || downloadingScenes) return;
|
||||
setDownloadingScenes(true);
|
||||
try {
|
||||
function extOf(url: string): string {
|
||||
if (url.startsWith("data:image/svg")) return "svg";
|
||||
if (url.startsWith("data:image/")) {
|
||||
return url.slice(11, url.indexOf(";")) || "png";
|
||||
}
|
||||
return "jpg";
|
||||
}
|
||||
// Main path + every unique alternate (AI-prefetched branches the player
|
||||
// didn't take). Dedupe by URL — the picked choice's alternate IS the
|
||||
// next main scene, so they overlap, and we never want the same image
|
||||
@@ -913,7 +845,7 @@ function GalleryInner() {
|
||||
sceneN++;
|
||||
files.push({
|
||||
url: sc.imageUrl,
|
||||
name: `infiplot-scene-${String(sceneN).padStart(3, "0")}.${extOf(sc.imageUrl)}`,
|
||||
name: `infiplot-scene-${String(sceneN).padStart(3, "0")}.${inferImageExtension(sc.imageUrl)}`,
|
||||
});
|
||||
}
|
||||
let branchN = 0;
|
||||
@@ -923,10 +855,10 @@ function GalleryInner() {
|
||||
branchN++;
|
||||
files.push({
|
||||
url: alt.imageUrl,
|
||||
name: `infiplot-branch-${String(branchN).padStart(3, "0")}.${extOf(alt.imageUrl)}`,
|
||||
name: `infiplot-branch-${String(branchN).padStart(3, "0")}.${inferImageExtension(alt.imageUrl)}`,
|
||||
});
|
||||
}
|
||||
await downloadImages(files);
|
||||
await downloadImagesAsZip(files, `infiplot-gallery-${doc.id}.zip`);
|
||||
} finally {
|
||||
setDownloadingScenes(false);
|
||||
}
|
||||
@@ -1000,7 +932,7 @@ function GalleryInner() {
|
||||
if (files.length === 0) return;
|
||||
setDownloadingPortraits(true);
|
||||
try {
|
||||
await downloadImages(files);
|
||||
await downloadImagesIndividually(files);
|
||||
} finally {
|
||||
setDownloadingPortraits(false);
|
||||
}
|
||||
@@ -1173,27 +1105,28 @@ function GalleryInner() {
|
||||
disabled={downloadingScenes}
|
||||
className="flex h-9 items-center gap-2 rounded-full bg-black/40 px-3 text-[11px] smallcaps text-white/80 backdrop-blur-sm transition-colors hover:text-white disabled:opacity-50"
|
||||
aria-label="批量下载图集到本地"
|
||||
title="把本局所有场景图(含未选中的分支预生成图)下载到本机(浏览器若弹「允许多个下载」请点允许)"
|
||||
title="把本局所有场景图(含未选中的分支预生成图)打包成 zip 下载到本机"
|
||||
>
|
||||
<i
|
||||
className={`fa-solid ${downloadingScenes ? "fa-spinner animate-spin" : "fa-download"} text-[11px]`}
|
||||
/>
|
||||
{downloadingScenes ? "下载中" : "下载图集"}
|
||||
{downloadingScenes ? "打包中" : "下载图集"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download-in-progress hint — Chrome/Edge/Firefox throw a "允许此网站
|
||||
下载多个文件" prompt after the first <a download>.click(); without
|
||||
this banner most users miss it and only the first file lands. */}
|
||||
{(downloadingScenes || downloadingPortraits) && (
|
||||
<div
|
||||
className="absolute inset-x-0 z-30 flex justify-center pointer-events-none px-4"
|
||||
style={{ top: "calc(max(0.75rem, env(safe-area-inset-top)) + 60px)" }}
|
||||
>
|
||||
<span className="flex items-center gap-2 rounded-full bg-black/70 px-4 py-2 text-[11px] text-white/95 backdrop-blur-sm shadow-lg max-w-[92vw]">
|
||||
<i className="fa-solid fa-circle-exclamation text-[11px] text-amber-300" />
|
||||
浏览器顶部如弹出「允许此网站下载多个文件」,请点「允许」,否则只能下到第一张
|
||||
<i
|
||||
className={`fa-solid ${downloadingScenes ? "fa-file-zipper" : "fa-circle-exclamation"} text-[11px] text-amber-300`}
|
||||
/>
|
||||
{downloadingScenes
|
||||
? "正在抓取图片并打包 zip,完成后会自动开始下载"
|
||||
: "浏览器顶部如弹出「允许此网站下载多个文件」,请点「允许」,否则只能下到第一张"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import JSZip from "jszip";
|
||||
|
||||
export type ImageZipFile = {
|
||||
url: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ImageZipDownloadResult = {
|
||||
downloaded: number;
|
||||
failed: ImageZipFile[];
|
||||
};
|
||||
|
||||
type DownloadOptions = {
|
||||
concurrency?: number;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
const DEFAULT_CONCURRENCY = 4;
|
||||
const DEFAULT_TIMEOUT_MS = 20_000;
|
||||
|
||||
export function inferImageExtension(url: string): string {
|
||||
if (url.startsWith("data:image/svg")) return "svg";
|
||||
if (url.startsWith("data:image/")) {
|
||||
return url.slice(11, url.indexOf(";")).replace(/^jpeg$/, "jpg") || "png";
|
||||
}
|
||||
|
||||
try {
|
||||
const ext = new URL(url).pathname.split(".").pop()?.toLowerCase();
|
||||
if (ext && ["jpg", "jpeg", "png", "webp", "gif", "svg"].includes(ext)) {
|
||||
return ext === "jpeg" ? "jpg" : ext;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the historical default used by gallery downloads.
|
||||
}
|
||||
|
||||
return "jpg";
|
||||
}
|
||||
|
||||
export async function downloadImagesAsZip(
|
||||
files: ImageZipFile[],
|
||||
zipName: string,
|
||||
options: DownloadOptions = {},
|
||||
): Promise<ImageZipDownloadResult> {
|
||||
const filtered = files.filter((file) => file.url && file.name);
|
||||
if (filtered.length === 0) return { downloaded: 0, failed: [] };
|
||||
|
||||
const blobs = await fetchImageBlobs(filtered, options);
|
||||
const zip = new JSZip();
|
||||
const usedPaths = new Set<string>();
|
||||
const failed: ImageZipFile[] = [];
|
||||
let downloaded = 0;
|
||||
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
const file = filtered[i]!;
|
||||
const blob = blobs[i];
|
||||
if (!blob) {
|
||||
failed.push(file);
|
||||
continue;
|
||||
}
|
||||
zip.file(uniqueZipPath(file.name, usedPaths), blob, { date: new Date() });
|
||||
downloaded++;
|
||||
}
|
||||
|
||||
if (downloaded === 0) return { downloaded, failed };
|
||||
|
||||
const blob = await zip.generateAsync({ type: "blob", compression: "STORE" });
|
||||
triggerBrowserDownload(blob, normalizeZipName(zipName));
|
||||
return { downloaded, failed };
|
||||
}
|
||||
|
||||
export async function downloadImagesIndividually(
|
||||
files: ImageZipFile[],
|
||||
options: DownloadOptions = {},
|
||||
): Promise<ImageZipDownloadResult> {
|
||||
const filtered = files.filter((file) => file.url && file.name);
|
||||
if (filtered.length === 0) return { downloaded: 0, failed: [] };
|
||||
|
||||
const blobs = await fetchImageBlobs(filtered, options);
|
||||
const failed: ImageZipFile[] = [];
|
||||
let downloaded = 0;
|
||||
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
const file = filtered[i]!;
|
||||
const blob = blobs[i];
|
||||
if (!blob) {
|
||||
failed.push(file);
|
||||
continue;
|
||||
}
|
||||
triggerBrowserDownload(blob, file.name);
|
||||
downloaded++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
|
||||
return { downloaded, failed };
|
||||
}
|
||||
|
||||
async function fetchImageBlobs(
|
||||
files: ImageZipFile[],
|
||||
options: DownloadOptions,
|
||||
): Promise<(Blob | null)[]> {
|
||||
const concurrency = Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY);
|
||||
const timeoutMs = Math.max(1000, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
const queue = files.map((file, index) => ({ file, index }));
|
||||
const blobs = new Array<Blob | null>(files.length).fill(null);
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(concurrency, files.length) }, async () => {
|
||||
while (queue.length > 0) {
|
||||
const next = queue.shift();
|
||||
if (!next) break;
|
||||
blobs[next.index] = await fetchImageBlob(next.file.url, timeoutMs);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return blobs;
|
||||
}
|
||||
|
||||
async function fetchImageBlob(url: string, timeoutMs: number): Promise<Blob | null> {
|
||||
if (!url) return null;
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||
try {
|
||||
const init: RequestInit = { signal: ctrl.signal };
|
||||
if (!url.startsWith("data:")) init.mode = "cors";
|
||||
const response = await fetch(url, init);
|
||||
if (!response.ok) return null;
|
||||
const blob = await response.blob();
|
||||
return blob.size > 0 ? blob : null;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerBrowserDownload(blob: Blob, fileName: string): void {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
a.download = fileName;
|
||||
a.rel = "noopener";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 1500);
|
||||
}
|
||||
|
||||
function normalizeZipName(name: string): string {
|
||||
const trimmed = name.trim() || "images.zip";
|
||||
return trimmed.toLowerCase().endsWith(".zip") ? trimmed : `${trimmed}.zip`;
|
||||
}
|
||||
|
||||
function uniqueZipPath(name: string, usedPaths: Set<string>): string {
|
||||
const clean = sanitizeZipPath(name);
|
||||
if (!usedPaths.has(clean)) {
|
||||
usedPaths.add(clean);
|
||||
return clean;
|
||||
}
|
||||
|
||||
const dot = clean.lastIndexOf(".");
|
||||
const base = dot > 0 ? clean.slice(0, dot) : clean;
|
||||
const ext = dot > 0 ? clean.slice(dot) : "";
|
||||
let n = 2;
|
||||
while (true) {
|
||||
const candidate = `${base}-${n}${ext}`;
|
||||
if (!usedPaths.has(candidate)) {
|
||||
usedPaths.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeZipPath(name: string): string {
|
||||
const parts = name
|
||||
.replace(/\\/g, "/")
|
||||
.split("/")
|
||||
.map((part) => part.replace(/[^\w.\-\u4e00-\u9fff]/g, "_"))
|
||||
.filter((part) => part && part !== "." && part !== "..");
|
||||
|
||||
return parts.join("/") || "image.jpg";
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
"@ai-sdk/openai": "^3.0.67",
|
||||
"ai": "^6.0.196",
|
||||
"jsonrepair": "^3.14.0",
|
||||
"jszip": "^3.10.1",
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
Generated
+75
@@ -23,6 +23,9 @@ importers:
|
||||
jsonrepair:
|
||||
specifier: ^3.14.0
|
||||
version: 3.14.0
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
next:
|
||||
specifier: ^16.0.0
|
||||
version: 16.2.7(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
@@ -1492,6 +1495,9 @@ packages:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1769,6 +1775,9 @@ packages:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
@@ -1806,6 +1815,9 @@ packages:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
@@ -1828,10 +1840,16 @@ packages:
|
||||
resolution: {integrity: sha512-tWPGKMZf/8UPim+fcW2EfcQ/d/7aKUrP6IECz9G3Tu6Q5dX0orSleqJ9z6sSw7qrQkjF8/Edo4DvsWBZ8H+HNg==}
|
||||
hasBin: true
|
||||
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
kleur@4.1.5:
|
||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -2013,6 +2031,9 @@ packages:
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -2115,6 +2136,9 @@ packages:
|
||||
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -2146,6 +2170,9 @@ packages:
|
||||
read-cache@1.0.0:
|
||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@@ -2166,6 +2193,9 @@ packages:
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
@@ -2185,6 +2215,9 @@ packages:
|
||||
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
@@ -2249,6 +2282,9 @@ packages:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4037,6 +4073,8 @@ snapshots:
|
||||
|
||||
cookie@1.1.1: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -4382,6 +4420,8 @@ snapshots:
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
@@ -4408,6 +4448,8 @@ snapshots:
|
||||
|
||||
is-stream@2.0.1: {}
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
isexe@3.1.5: {}
|
||||
@@ -4422,8 +4464,19 @@ snapshots:
|
||||
|
||||
jsonrepair@3.14.0: {}
|
||||
|
||||
jszip@3.10.1:
|
||||
dependencies:
|
||||
lie: 3.3.0
|
||||
pako: 1.0.11
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
kleur@4.1.5: {}
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
@@ -4566,6 +4619,8 @@ snapshots:
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-expression-matcher@1.5.0: {}
|
||||
@@ -4644,6 +4699,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
@@ -4675,6 +4732,16 @@ snapshots:
|
||||
dependencies:
|
||||
pify: 2.3.0
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.2
|
||||
@@ -4702,6 +4769,8 @@ snapshots:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
||||
safe-buffer@5.1.2: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
@@ -4733,6 +4802,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
sharp@0.33.5:
|
||||
@@ -4851,6 +4922,10 @@ snapshots:
|
||||
get-east-asian-width: 1.6.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
Reference in New Issue
Block a user