Files
DndGamePlayer/app/main/protocol/dndAssetProtocol.ts
T
Ivan Fontosh cbb6edc378 chore(release): gate users-branch features for hotfix
Add USERS_BRANCH_FEATURES_ENABLED (off by default) to hide Players library, session tokens, and related UI while keeping the preview import fix. Restore primary Run button when the flag is off.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 10:15:28 +08:00

106 lines
3.4 KiB
TypeScript

import fs from 'node:fs/promises';
import { session } from 'electron';
import { asAssetId, asPlayerId, asTokenId } from '../../shared/types/ids';
import type { PlayersStore } from '../players/playersStore';
import type { ZipProjectStore } from '../project/zipStore';
import type { TokensStore } from '../tokens/tokensStore';
type ReadInfo = { absPath: string; mime: string };
async function serveFile(info: ReadInfo, request: Request): Promise<Response> {
try {
const stat = await fs.stat(info.absPath);
const total = stat.size;
const range = request.headers.get('range') ?? request.headers.get('Range');
if (range) {
const m = /^bytes=(\d+)-(\d+)?$/iu.exec(range.trim());
if (m) {
const start = Number(m[1]);
const endRaw = m[2] ? Number(m[2]) : total - 1;
const end = Math.min(endRaw, total - 1);
if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || start >= total || end < start) {
return new Response(null, {
status: 416,
headers: {
'Content-Range': `bytes */${String(total)}`,
'Cache-Control': 'no-store',
},
});
}
const len = end - start + 1;
const fh = await fs.open(info.absPath, 'r');
try {
const buf = Buffer.alloc(len);
const { bytesRead } = await fh.read(buf, 0, len, start);
if (bytesRead <= 0) {
return new Response(null, {
status: 416,
headers: {
'Content-Range': `bytes */${String(total)}`,
'Cache-Control': 'no-store',
},
});
}
const body = bytesRead === len ? buf : Buffer.from(buf.subarray(0, bytesRead));
const actualEnd = start + bytesRead - 1;
return new Response(body, {
status: 206,
headers: {
'Content-Type': info.mime,
'Accept-Ranges': 'bytes',
'Content-Range': `bytes ${String(start)}-${String(actualEnd)}/${String(total)}`,
'Content-Length': String(body.length),
'Cache-Control': 'no-store',
},
});
} finally {
await fh.close();
}
}
}
const buf = await fs.readFile(info.absPath);
return new Response(buf, {
headers: {
'Content-Type': info.mime,
'Accept-Ranges': 'bytes',
'Cache-Control': 'no-store',
},
});
} catch {
return new Response(null, { status: 404 });
}
}
/**
* Обслуживает `dnd://asset?...`, `dnd://token?...` и `dnd://player?...`.
*/
export function registerDndAssetProtocol(
projectStore: ZipProjectStore,
tokensStore: TokensStore,
playersStore: PlayersStore | null,
): void {
session.defaultSession.protocol.handle('dnd', async (request) => {
const url = new URL(request.url);
const id = url.searchParams.get('id');
if (!id) {
return new Response(null, { status: 404 });
}
let info: ReadInfo | null = null;
if (url.hostname === 'asset') {
info = projectStore.getAssetReadInfo(asAssetId(id));
} else if (url.hostname === 'token') {
info = tokensStore.getImageReadInfo(asTokenId(id));
} else if (url.hostname === 'player') {
info = playersStore?.getImageReadInfo(asPlayerId(id)) ?? null;
}
if (!info) {
return new Response(null, { status: 404 });
}
return serveFile(info, request);
});
}