feat(tokens): app-local non-player tokens with session moves and UI polish
Add token library/placements, keep play-time moves for the session, lock presentation interactions, and fix export/import modal layout plus freeform trap label. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,38 +2,39 @@ import fs from 'node:fs/promises';
|
||||
|
||||
import { session } from 'electron';
|
||||
|
||||
import { asAssetId } from '../../shared/types/ids';
|
||||
import { asAssetId, asTokenId } from '../../shared/types/ids';
|
||||
import type { ZipProjectStore } from '../project/zipStore';
|
||||
import type { TokensStore } from '../tokens/tokensStore';
|
||||
|
||||
/**
|
||||
* Обслуживает `dnd://asset?...` — без этого `<img src="file://...">` в рендерере часто ломается.
|
||||
*/
|
||||
export function registerDndAssetProtocol(projectStore: ZipProjectStore): void {
|
||||
session.defaultSession.protocol.handle('dnd', async (request) => {
|
||||
const url = new URL(request.url);
|
||||
if (url.hostname !== 'asset') {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
const id = url.searchParams.get('id');
|
||||
if (!id) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
const info = projectStore.getAssetReadInfo(asAssetId(id));
|
||||
if (!info) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
try {
|
||||
const stat = await fs.stat(info.absPath);
|
||||
const total = stat.size;
|
||||
const range = request.headers.get('range') ?? request.headers.get('Range');
|
||||
type ReadInfo = { absPath: string; mime: string };
|
||||
|
||||
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) {
|
||||
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: {
|
||||
@@ -42,48 +43,59 @@ export function registerDndAssetProtocol(projectStore: ZipProjectStore): void {
|
||||
},
|
||||
});
|
||||
}
|
||||
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 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 {
|
||||
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?...`.
|
||||
*/
|
||||
export function registerDndAssetProtocol(
|
||||
projectStore: ZipProjectStore,
|
||||
tokensStore: TokensStore,
|
||||
): 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));
|
||||
}
|
||||
if (!info) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
return serveFile(info, request);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user