pixelkiln 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/SECURITY.md +12 -2
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +1158 -307
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +512 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -3
- package/dist/index.d.ts +101 -3
- package/dist/index.js +525 -44
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +16 -0
- package/docs/ARTIFACTS.md +25 -0
- package/docs/CLI.md +56 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -9393,7 +9393,7 @@ function validateWorkspace(ws, dir) {
|
|
|
9393
9393
|
|
|
9394
9394
|
// src/gallery/edit.ts
|
|
9395
9395
|
var import_node_path24 = __toESM(require("path"), 1);
|
|
9396
|
-
var
|
|
9396
|
+
var import_zod9 = require("zod");
|
|
9397
9397
|
|
|
9398
9398
|
// src/manifest-edit.ts
|
|
9399
9399
|
var import_promises18 = require("fs/promises");
|
|
@@ -9649,6 +9649,7 @@ var import_node_child_process3 = require("child_process");
|
|
|
9649
9649
|
var import_node_fs20 = require("fs");
|
|
9650
9650
|
var import_promises19 = require("fs/promises");
|
|
9651
9651
|
var import_node_path23 = __toESM(require("path"), 1);
|
|
9652
|
+
var import_zod8 = require("zod");
|
|
9652
9653
|
var HAND_EDIT_DIR = "edits";
|
|
9653
9654
|
function handEditPath(loaded, spec) {
|
|
9654
9655
|
const style = loaded.manifest.styles[spec.styleId];
|
|
@@ -9718,6 +9719,89 @@ async function detachHandEdit(loaded, spec, opts = {}) {
|
|
|
9718
9719
|
});
|
|
9719
9720
|
return { manifestSha256: result.sha256, changed: result.changed };
|
|
9720
9721
|
}
|
|
9722
|
+
var HandEditCompanionSchema = import_zod8.z.object({
|
|
9723
|
+
version: import_zod8.z.literal(1),
|
|
9724
|
+
/** sha256 of the generated art the edit started from; null when it started from untracked art. */
|
|
9725
|
+
basedOn: import_zod8.z.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
9726
|
+
/** sha256 of the edit PNG as saved, so a later change by another tool is visible. */
|
|
9727
|
+
sha256: import_zod8.z.string().regex(/^[0-9a-f]{64}$/),
|
|
9728
|
+
/** Editor identity the bridge reported, e.g. `pixelorama@v1.2.2-stable`. */
|
|
9729
|
+
editor: import_zod8.z.string().min(1),
|
|
9730
|
+
protocol: import_zod8.z.number().int().positive(),
|
|
9731
|
+
savedAt: import_zod8.z.string().datetime(),
|
|
9732
|
+
/** Basename of the layered project file beside the edit, when one was kept. */
|
|
9733
|
+
project: import_zod8.z.string().min(1).nullable()
|
|
9734
|
+
}).strict();
|
|
9735
|
+
function handEditCompanionPath(editPath) {
|
|
9736
|
+
return editPath.replace(/\.png$/i, "") + ".edit.json";
|
|
9737
|
+
}
|
|
9738
|
+
function handEditProjectPath(editPath) {
|
|
9739
|
+
return editPath.replace(/\.png$/i, "") + ".pxo";
|
|
9740
|
+
}
|
|
9741
|
+
async function readHandEditCompanion(editPath) {
|
|
9742
|
+
let text;
|
|
9743
|
+
try {
|
|
9744
|
+
text = await (0, import_promises19.readFile)(handEditCompanionPath(editPath), "utf8");
|
|
9745
|
+
} catch {
|
|
9746
|
+
return null;
|
|
9747
|
+
}
|
|
9748
|
+
try {
|
|
9749
|
+
return HandEditCompanionSchema.parse(JSON.parse(text));
|
|
9750
|
+
} catch {
|
|
9751
|
+
return null;
|
|
9752
|
+
}
|
|
9753
|
+
}
|
|
9754
|
+
var MAX_HAND_EDIT_BYTES = 16 * 1024 * 1024;
|
|
9755
|
+
async function saveHandEdit(loaded, lock, spec, input) {
|
|
9756
|
+
if (input.png.length > MAX_HAND_EDIT_BYTES) {
|
|
9757
|
+
throw new ManifestEditError(`the edit is ${input.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
9758
|
+
}
|
|
9759
|
+
if (input.project && input.project.length > MAX_HAND_EDIT_BYTES) {
|
|
9760
|
+
throw new ManifestEditError(`the project file is ${input.project.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
9761
|
+
}
|
|
9762
|
+
let decoded;
|
|
9763
|
+
try {
|
|
9764
|
+
validateMedia(input.png, MediaType.PNG);
|
|
9765
|
+
decoded = decodePng(input.png);
|
|
9766
|
+
} catch (error) {
|
|
9767
|
+
throw new ManifestEditError(`the edit is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
|
|
9768
|
+
}
|
|
9769
|
+
const base = handEditBase(spec, lock);
|
|
9770
|
+
const expected = base ? decodePng(await (0, import_promises19.readFile)(base)) : { width: spec.width, height: spec.height };
|
|
9771
|
+
if (decoded.width !== expected.width || decoded.height !== expected.height) {
|
|
9772
|
+
throw new ManifestEditError(
|
|
9773
|
+
`the edit is ${decoded.width}\xD7${decoded.height}; ${spec.styleId}/${spec.assetId} is ${expected.width}\xD7${expected.height}`
|
|
9774
|
+
);
|
|
9775
|
+
}
|
|
9776
|
+
const started = await startHandEdit(loaded, lock, spec, { expectedSha256: input.expectedSha256 });
|
|
9777
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9778
|
+
const basedOn = entry ? primaryOutput(entry)?.sha256 ?? null : null;
|
|
9779
|
+
const digest2 = sha256(input.png);
|
|
9780
|
+
const projectPath = input.project ? handEditProjectPath(started.editPath) : null;
|
|
9781
|
+
const companion = {
|
|
9782
|
+
version: 1,
|
|
9783
|
+
basedOn,
|
|
9784
|
+
sha256: digest2,
|
|
9785
|
+
editor: input.editor,
|
|
9786
|
+
protocol: input.protocol,
|
|
9787
|
+
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
9788
|
+
project: projectPath ? import_node_path23.default.basename(projectPath) : null
|
|
9789
|
+
};
|
|
9790
|
+
await replaceFile(started.editPath, input.png);
|
|
9791
|
+
if (projectPath && input.project) await replaceFile(projectPath, input.project);
|
|
9792
|
+
const companionPath = handEditCompanionPath(started.editPath);
|
|
9793
|
+
await replaceFile(companionPath, JSON.stringify(companion, null, 2) + "\n");
|
|
9794
|
+
return { ...started, sha256: digest2, basedOn, companionPath, projectPath };
|
|
9795
|
+
}
|
|
9796
|
+
async function replaceFile(file, bytes) {
|
|
9797
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
9798
|
+
try {
|
|
9799
|
+
await (0, import_promises19.writeFile)(tmp, bytes);
|
|
9800
|
+
await (0, import_promises19.rename)(tmp, file);
|
|
9801
|
+
} finally {
|
|
9802
|
+
await (0, import_promises19.rm)(tmp, { force: true });
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9721
9805
|
function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
9722
9806
|
const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
|
|
9723
9807
|
const [program, ...args] = command;
|
|
@@ -9729,22 +9813,37 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
|
9729
9813
|
}
|
|
9730
9814
|
|
|
9731
9815
|
// src/gallery/edit.ts
|
|
9732
|
-
var
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9816
|
+
var Base64 = import_zod9.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
9817
|
+
var HandEditRequestSchema = import_zod9.z.discriminatedUnion("action", [
|
|
9818
|
+
import_zod9.z.object({
|
|
9819
|
+
action: import_zod9.z.literal("start-edit"),
|
|
9820
|
+
project: import_zod9.z.string().min(1).optional(),
|
|
9821
|
+
assetId: import_zod9.z.string().min(1),
|
|
9822
|
+
styleId: import_zod9.z.string().min(1),
|
|
9823
|
+
expectedSha256: import_zod9.z.string().regex(/^[0-9a-f]{64}$/),
|
|
9739
9824
|
/** Also launch the author's editor on the file. */
|
|
9740
|
-
open:
|
|
9825
|
+
open: import_zod9.z.boolean().optional()
|
|
9741
9826
|
}).strict(),
|
|
9742
|
-
|
|
9743
|
-
action:
|
|
9744
|
-
project:
|
|
9745
|
-
assetId:
|
|
9746
|
-
styleId:
|
|
9747
|
-
expectedSha256:
|
|
9827
|
+
import_zod9.z.object({
|
|
9828
|
+
action: import_zod9.z.literal("detach-edit"),
|
|
9829
|
+
project: import_zod9.z.string().min(1).optional(),
|
|
9830
|
+
assetId: import_zod9.z.string().min(1),
|
|
9831
|
+
styleId: import_zod9.z.string().min(1),
|
|
9832
|
+
expectedSha256: import_zod9.z.string().regex(/^[0-9a-f]{64}$/)
|
|
9833
|
+
}).strict(),
|
|
9834
|
+
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
9835
|
+
import_zod9.z.object({
|
|
9836
|
+
action: import_zod9.z.literal("save-edit"),
|
|
9837
|
+
project: import_zod9.z.string().min(1).optional(),
|
|
9838
|
+
assetId: import_zod9.z.string().min(1),
|
|
9839
|
+
styleId: import_zod9.z.string().min(1),
|
|
9840
|
+
expectedSha256: import_zod9.z.string().regex(/^[0-9a-f]{64}$/),
|
|
9841
|
+
png: Base64.min(1),
|
|
9842
|
+
/** The editor's layered project file, kept beside the edit. */
|
|
9843
|
+
pxo: Base64.optional(),
|
|
9844
|
+
/** Editor identity from the bridge's ready message. */
|
|
9845
|
+
editor: import_zod9.z.string().min(1).max(200),
|
|
9846
|
+
protocol: import_zod9.z.number().int().positive()
|
|
9748
9847
|
}).strict()
|
|
9749
9848
|
]);
|
|
9750
9849
|
function createGalleryEditHandler(opts) {
|
|
@@ -9765,6 +9864,15 @@ function createGalleryEditHandler(opts) {
|
|
|
9765
9864
|
const command = (opts.openEditor ?? openInEditor)(started.editPath);
|
|
9766
9865
|
log(` opened with: ${command}`);
|
|
9767
9866
|
}
|
|
9867
|
+
} else if (request.action === "save-edit") {
|
|
9868
|
+
const saved = await saveHandEdit(ctx.loaded, ctx.lock, spec, {
|
|
9869
|
+
png: Buffer.from(request.png, "base64"),
|
|
9870
|
+
...request.pxo ? { project: Buffer.from(request.pxo, "base64") } : {},
|
|
9871
|
+
editor: request.editor,
|
|
9872
|
+
protocol: request.protocol,
|
|
9873
|
+
expectedSha256: request.expectedSha256
|
|
9874
|
+
});
|
|
9875
|
+
log(` hand edit saved from ${request.editor}: ${saved.source}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
|
|
9768
9876
|
} else {
|
|
9769
9877
|
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
9770
9878
|
if (result2.changed) log(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
@@ -9795,6 +9903,7 @@ function createGalleryEditHandler(opts) {
|
|
|
9795
9903
|
}
|
|
9796
9904
|
|
|
9797
9905
|
// src/gallery/snapshot.ts
|
|
9906
|
+
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
9798
9907
|
function galleryMediaId(absolutePath) {
|
|
9799
9908
|
return (0, import_node_crypto6.createHash)("sha256").update(import_node_path25.default.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
9800
9909
|
}
|
|
@@ -9929,6 +10038,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9929
10038
|
let outputs;
|
|
9930
10039
|
let edit = null;
|
|
9931
10040
|
let editStatus = null;
|
|
10041
|
+
let editMeta = null;
|
|
9932
10042
|
if (entry && entry.outputs.length) {
|
|
9933
10043
|
outputs = await Promise.all(
|
|
9934
10044
|
entry.outputs.map(
|
|
@@ -9940,7 +10050,27 @@ async function buildGallerySnapshot(opts) {
|
|
|
9940
10050
|
const editSha = (0, import_node_fs21.existsSync)(editPath) ? await sha256File(editPath) : null;
|
|
9941
10051
|
edit = await describeOutput(media, root, editPath, { sha256: editSha });
|
|
9942
10052
|
const generated = outputs[0];
|
|
9943
|
-
|
|
10053
|
+
const companion = await readHandEditCompanion(editPath);
|
|
10054
|
+
if (companion) {
|
|
10055
|
+
const projectPath = handEditProjectPath(editPath);
|
|
10056
|
+
const project = companion.project && (0, import_node_fs21.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
|
|
10057
|
+
let projectUrl = null;
|
|
10058
|
+
if (project) {
|
|
10059
|
+
const id = galleryMediaId(projectPath);
|
|
10060
|
+
media.set(id, { path: projectPath, contentType: PROJECT_FILE_TYPE });
|
|
10061
|
+
projectUrl = `${galleryMediaRoute(id)}?v=${project.bytes}-${Date.parse(project.modifiedAt).toString(36)}`;
|
|
10062
|
+
}
|
|
10063
|
+
editMeta = {
|
|
10064
|
+
editor: companion.editor,
|
|
10065
|
+
savedAt: companion.savedAt,
|
|
10066
|
+
basedOn: companion.basedOn,
|
|
10067
|
+
changedSince: editSha !== null && editSha !== companion.sha256,
|
|
10068
|
+
project: project ? portableOutputPath(projectPath, root) : null,
|
|
10069
|
+
projectUrl
|
|
10070
|
+
};
|
|
10071
|
+
}
|
|
10072
|
+
const regenerated = companion ? generated?.sha256 !== void 0 && generated.sha256 !== null && companion.basedOn !== generated.sha256 : Boolean(generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists);
|
|
10073
|
+
editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
9944
10074
|
}
|
|
9945
10075
|
} else if (spec.source) {
|
|
9946
10076
|
outputs = [await describeOutput(media, root, import_node_path25.default.resolve(root, spec.source))];
|
|
@@ -9990,6 +10120,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9990
10120
|
source: spec.source ?? null,
|
|
9991
10121
|
edit,
|
|
9992
10122
|
editStatus,
|
|
10123
|
+
editMeta,
|
|
9993
10124
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
9994
10125
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
9995
10126
|
tags: spec.tags,
|
|
@@ -10046,6 +10177,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10046
10177
|
source: null,
|
|
10047
10178
|
edit: null,
|
|
10048
10179
|
editStatus: null,
|
|
10180
|
+
editMeta: null,
|
|
10049
10181
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
10050
10182
|
refreshable: false,
|
|
10051
10183
|
tags: [],
|
|
@@ -10247,6 +10379,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10247
10379
|
const session = JSON.stringify(opts.session ?? null);
|
|
10248
10380
|
const editable = JSON.stringify(Boolean(opts.editable && opts.session));
|
|
10249
10381
|
const generation = JSON.stringify(Boolean(opts.generation && opts.session));
|
|
10382
|
+
const editor = JSON.stringify(Boolean(opts.editor && opts.session));
|
|
10250
10383
|
const title = `pixelkiln \u2014 ${snapshot.project?.name ?? "workspace"}`;
|
|
10251
10384
|
return `<!doctype html>
|
|
10252
10385
|
<html lang="en">
|
|
@@ -10452,6 +10585,15 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10452
10585
|
.job .acts button { padding:3px 9px; font-size:12px; }
|
|
10453
10586
|
.job pre { grid-column:1 / -1; margin:0; padding:8px 10px; border-top:1px solid var(--line);
|
|
10454
10587
|
font:11.5px/1.45 var(--mono); color:var(--dim); max-height:220px; overflow:auto; white-space:pre-wrap; }
|
|
10588
|
+
#tools { width:min(100%,var(--content)); margin:0 auto; padding:0 22px 12px; }
|
|
10589
|
+
#tools:empty { display:none; }
|
|
10590
|
+
.tool { border:1px solid var(--line); background:var(--panel-deep); padding:8px 12px; display:grid;
|
|
10591
|
+
grid-template-columns:auto 1fr auto; gap:6px 14px; align-items:center; font-size:12.5px; }
|
|
10592
|
+
.tool .ph { display:inline-flex; align-items:center; gap:6px; font:650 12px/1 var(--mono); white-space:nowrap; }
|
|
10593
|
+
.tool .last { color:var(--dim); min-width:0; }
|
|
10594
|
+
.tool .acts { display:flex; gap:6px; }
|
|
10595
|
+
.tool .acts button { padding:3px 9px; font-size:12px; }
|
|
10596
|
+
.tool progress { grid-column:1 / -1; width:100%; height:6px; accent-color:var(--accent); }
|
|
10455
10597
|
.budget { color:var(--dim); }
|
|
10456
10598
|
.budget b { color:var(--text); }
|
|
10457
10599
|
.dialog { position:fixed; inset:0; z-index:30; display:grid; place-items:center; background:rgba(0,0,0,.55); }
|
|
@@ -10472,6 +10614,16 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10472
10614
|
border-left:1px solid var(--line-strong); display:grid; grid-template-rows:auto 1fr; min-width:0; }
|
|
10473
10615
|
.sheet.review-host { width:min(1180px, 94vw); }
|
|
10474
10616
|
.sheet.compare-host { width:min(1500px, 96vw); }
|
|
10617
|
+
.sheet.editor-host { width:min(1400px, 96vw); grid-template-rows:auto 1fr; }
|
|
10618
|
+
.sheet.editor-host .rbar b { font:650 12.5px/1.4 var(--mono); color:var(--text); }
|
|
10619
|
+
.sheet.editor-host .rbar .acts { margin-left:auto; display:flex; gap:8px; align-items:center; }
|
|
10620
|
+
.sheet.editor-host .rbar .st { display:inline-flex; align-items:center; gap:6px; font:650 11.5px/1 var(--mono); }
|
|
10621
|
+
.sheet.editor-host .rbar .msg { color:var(--bad); font-size:12.5px; }
|
|
10622
|
+
.sheet.editor-host .stage { position:relative; min-height:0; }
|
|
10623
|
+
.sheet.editor-host .stage iframe { position:absolute; inset:0; }
|
|
10624
|
+
.sheet.editor-host .loading { position:absolute; inset:0; display:grid; place-items:center; background:var(--bg);
|
|
10625
|
+
color:var(--dim); text-align:center; padding:24px; }
|
|
10626
|
+
.sheet.editor-host .loading b { display:block; color:var(--text); margin-bottom:6px; }
|
|
10475
10627
|
.sheet .rbar { display:flex; align-items:center; gap:12px; padding:10px 16px; border-bottom:1px solid var(--line-strong);
|
|
10476
10628
|
background:var(--panel); font-size:13px; flex-wrap:wrap; }
|
|
10477
10629
|
.sheet .rbar span { color:var(--dim); }
|
|
@@ -10510,7 +10662,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10510
10662
|
.tray .pick img { width:22px; height:22px; object-fit:contain; image-rendering:pixelated; }
|
|
10511
10663
|
.tray .pick button { padding:0 5px; border-color:transparent; color:var(--dim); font-size:12px; }
|
|
10512
10664
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
10513
|
-
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
|
|
10665
|
+
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
10514
10666
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
10515
10667
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
10516
10668
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
@@ -10558,6 +10710,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10558
10710
|
<div class="totals" id="totals"></div>
|
|
10559
10711
|
<div class="chips" id="chips"></div>
|
|
10560
10712
|
<div id="jobs"></div>
|
|
10713
|
+
<div id="tools"></div>
|
|
10561
10714
|
</header>
|
|
10562
10715
|
<main id="root"></main>
|
|
10563
10716
|
<footer>
|
|
@@ -10576,6 +10729,7 @@ const INITIAL = ${data};
|
|
|
10576
10729
|
const SESSION = ${session};
|
|
10577
10730
|
const EDITABLE = ${editable};
|
|
10578
10731
|
const GENERATION = ${generation};
|
|
10732
|
+
const EDITOR = ${editor};
|
|
10579
10733
|
let snap = INITIAL;
|
|
10580
10734
|
const STATE_TONE = {
|
|
10581
10735
|
ok: 'ok', stale: 'warn', orphaned: 'warn', untracked: 'warn', blocked: 'warn',
|
|
@@ -10743,6 +10897,92 @@ function renderJobs() {
|
|
|
10743
10897
|
host.append(row);
|
|
10744
10898
|
}
|
|
10745
10899
|
}
|
|
10900
|
+
// ---- the in-browser editor (only when the server serves one) -------------
|
|
10901
|
+
// The build is a 46 MB web export fetched once into a user-level cache and
|
|
10902
|
+
// verified against hashes this PixelKiln release carries; the page only
|
|
10903
|
+
// reports and asks, the server downloads. Nothing is fetched without a click.
|
|
10904
|
+
|
|
10905
|
+
let ED = null;
|
|
10906
|
+
let edTimer = null;
|
|
10907
|
+
const fmtMb = (bytes) => (bytes / 1e6).toFixed(1) + ' MB';
|
|
10908
|
+
async function pollEditor() {
|
|
10909
|
+
if (!EDITOR) return;
|
|
10910
|
+
try {
|
|
10911
|
+
const res = await fetch('/api/editor', { cache: 'no-store' });
|
|
10912
|
+
if (!res.ok) throw new Error(await res.text());
|
|
10913
|
+
ED = await res.json();
|
|
10914
|
+
} catch (err) {
|
|
10915
|
+
ED = { error: 'status unavailable: ' + err.message, installed: false, installing: null, missing: [], totalBytes: 0 };
|
|
10916
|
+
}
|
|
10917
|
+
renderTools();
|
|
10918
|
+
clearTimeout(edTimer);
|
|
10919
|
+
if (ED.installing) edTimer = setTimeout(pollEditor, 1000);
|
|
10920
|
+
else if (ui.open) renderDrawer();
|
|
10921
|
+
}
|
|
10922
|
+
async function installEditor() {
|
|
10923
|
+
try {
|
|
10924
|
+
const res = await fetch('/api/editor/install', {
|
|
10925
|
+
method: 'POST',
|
|
10926
|
+
headers: { 'Content-Type': 'application/json', 'X-Pixelkiln-Session': SESSION },
|
|
10927
|
+
body: '{}',
|
|
10928
|
+
});
|
|
10929
|
+
if (!res.ok) throw new Error(await res.text());
|
|
10930
|
+
ED = await res.json();
|
|
10931
|
+
} catch (err) {
|
|
10932
|
+
ED = Object.assign({}, ED, { error: err.message });
|
|
10933
|
+
}
|
|
10934
|
+
renderTools();
|
|
10935
|
+
clearTimeout(edTimer);
|
|
10936
|
+
edTimer = setTimeout(pollEditor, 500);
|
|
10937
|
+
}
|
|
10938
|
+
function renderTools() {
|
|
10939
|
+
const host = $('tools');
|
|
10940
|
+
host.textContent = '';
|
|
10941
|
+
if (!EDITOR || !ED) return;
|
|
10942
|
+
const row = el('div', 'tool');
|
|
10943
|
+
const ph = el('span', 'ph');
|
|
10944
|
+
const last = el('span', 'last');
|
|
10945
|
+
const acts = el('div', 'acts');
|
|
10946
|
+
const name = 'Browser editor' + (ED.pixelorama ? ' (Pixelorama ' + ED.pixelorama + ')' : '');
|
|
10947
|
+
if (ED.installing) {
|
|
10948
|
+
const p = ED.installing;
|
|
10949
|
+
ph.append(el('i', 'dot cool'), document.createTextNode('installing'));
|
|
10950
|
+
last.textContent = name + ': fetching ' + (p.file || 'the build') + ' \u2014 ' + fmtMb(p.fetchedBytes) + ' of ' + fmtMb(p.totalBytes);
|
|
10951
|
+
row.append(ph, last, acts);
|
|
10952
|
+
const bar = el('progress');
|
|
10953
|
+
if (p.totalBytes) { bar.max = p.totalBytes; bar.value = p.fetchedBytes; }
|
|
10954
|
+
row.append(bar);
|
|
10955
|
+
} else if (ED.installed) {
|
|
10956
|
+
ph.append(el('i', 'dot ok'), document.createTextNode('ready'));
|
|
10957
|
+
last.textContent = name + ' is installed and verified; a record\u2019s Hand edit section opens sprites in it.';
|
|
10958
|
+
last.title = ED.dir || '';
|
|
10959
|
+
const b = el('button', null, 'Open editor');
|
|
10960
|
+
b.type = 'button'; b.onclick = () => window.open(ED.url, '_blank', 'noopener');
|
|
10961
|
+
acts.append(b);
|
|
10962
|
+
row.append(ph, last, acts);
|
|
10963
|
+
} else if (ED.error) {
|
|
10964
|
+
ph.append(el('i', 'dot bad'), document.createTextNode('failed'));
|
|
10965
|
+
last.textContent = name + ': ' + ED.error;
|
|
10966
|
+
const b = el('button', 'primary', 'Retry install');
|
|
10967
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
10968
|
+
acts.append(b);
|
|
10969
|
+
row.append(ph, last, acts);
|
|
10970
|
+
} else if (!ED.release) {
|
|
10971
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('unavailable'));
|
|
10972
|
+
last.textContent = name + ': this PixelKiln version pins no published build.';
|
|
10973
|
+
row.append(ph, last, acts);
|
|
10974
|
+
} else {
|
|
10975
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('not installed'));
|
|
10976
|
+
last.textContent = name + ': ' + fmtMb(ED.totalBytes - (ED.installedBytes || 0)) + ' fetched once from the PixelKiln release ' +
|
|
10977
|
+
ED.release + ', verified against the hashes this version carries, and kept outside the project.';
|
|
10978
|
+
const b = el('button', 'primary', 'Install editor');
|
|
10979
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
10980
|
+
acts.append(b);
|
|
10981
|
+
row.append(ph, last, acts);
|
|
10982
|
+
}
|
|
10983
|
+
host.append(row);
|
|
10984
|
+
}
|
|
10985
|
+
|
|
10746
10986
|
function budgetLine() {
|
|
10747
10987
|
const providers = new Set([...Object.keys(GEN.budget.byProvider), ...Object.keys(GEN.spent)]);
|
|
10748
10988
|
const parts = [];
|
|
@@ -10849,8 +11089,183 @@ function openReview(jobId) {
|
|
|
10849
11089
|
host.append(scrim, panel);
|
|
10850
11090
|
}
|
|
10851
11091
|
window.addEventListener('message', (e) => {
|
|
10852
|
-
if (e.origin !== location.origin || !e.data || e.data.type !== '
|
|
10853
|
-
|
|
11092
|
+
if (e.origin !== location.origin || !e.data || typeof e.data.type !== 'string') return;
|
|
11093
|
+
if (e.data.type === 'pixelkiln:review-applied') {
|
|
11094
|
+
setTimeout(() => { $('dialog-host').textContent = ''; pollJobs(); refresh(); }, 600);
|
|
11095
|
+
return;
|
|
11096
|
+
}
|
|
11097
|
+
if (SHEET && e.source === SHEET.frame.contentWindow) onEditorMessage(e.data);
|
|
11098
|
+
});
|
|
11099
|
+
|
|
11100
|
+
// ---- the in-browser editor sheet -------------------------------------------
|
|
11101
|
+
// The page is the protocol host: it hands the editor the PNG and palette,
|
|
11102
|
+
// asks it for the image back, and does the authenticated write itself. The
|
|
11103
|
+
// iframe never sees the session token, and the server validates, sizes, and
|
|
11104
|
+
// declares the file the same way pixelkiln edit does.
|
|
11105
|
+
|
|
11106
|
+
let SHEET = null;
|
|
11107
|
+
const editorSource = (item) => item.edit && item.edit.exists ? item.edit
|
|
11108
|
+
: item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png' ? item.outputs[0] : null;
|
|
11109
|
+
const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSource(item) &&
|
|
11110
|
+
!(item.outputs.length > 1);
|
|
11111
|
+
const toBase64 = (buf) => new Promise((resolve, reject) => {
|
|
11112
|
+
const r = new FileReader();
|
|
11113
|
+
r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
|
|
11114
|
+
r.onerror = () => reject(r.error);
|
|
11115
|
+
r.readAsDataURL(new Blob([buf]));
|
|
11116
|
+
});
|
|
11117
|
+
function sheetStatus(tone, text) {
|
|
11118
|
+
if (!SHEET) return;
|
|
11119
|
+
SHEET.status.textContent = '';
|
|
11120
|
+
SHEET.status.append(el('i', 'dot ' + tone), document.createTextNode(text));
|
|
11121
|
+
}
|
|
11122
|
+
function sheetButtons() {
|
|
11123
|
+
if (!SHEET) return;
|
|
11124
|
+
const busy = !SHEET.opened || SHEET.pending !== null;
|
|
11125
|
+
SHEET.save.disabled = busy || !SHEET.dirty;
|
|
11126
|
+
SHEET.saveClose.disabled = busy;
|
|
11127
|
+
}
|
|
11128
|
+
function openEditorSheet(item) {
|
|
11129
|
+
if (SHEET) return;
|
|
11130
|
+
const src = editorSource(item);
|
|
11131
|
+
const host = $('dialog-host');
|
|
11132
|
+
host.textContent = '';
|
|
11133
|
+
const scrim = el('div', 'sheet-scrim');
|
|
11134
|
+
scrim.onclick = () => closeEditorSheet();
|
|
11135
|
+
const panel = el('div', 'sheet editor-host');
|
|
11136
|
+
const bar = el('div', 'rbar');
|
|
11137
|
+
const close = el('button', null, 'Close'); close.type = 'button';
|
|
11138
|
+
close.onclick = () => closeEditorSheet();
|
|
11139
|
+
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height);
|
|
11140
|
+
const status = el('span', 'st');
|
|
11141
|
+
const acts = el('div', 'acts');
|
|
11142
|
+
const msg = el('span', 'msg');
|
|
11143
|
+
const save = el('button', 'primary', 'Save to project'); save.type = 'button';
|
|
11144
|
+
save.onclick = () => requestEditorSave(false);
|
|
11145
|
+
const saveClose = el('button', null, 'Save & close'); saveClose.type = 'button';
|
|
11146
|
+
saveClose.onclick = () => requestEditorSave(true);
|
|
11147
|
+
acts.append(msg, save, saveClose);
|
|
11148
|
+
bar.append(close, title, status, acts);
|
|
11149
|
+
const stage = el('div', 'stage');
|
|
11150
|
+
const frame = el('iframe');
|
|
11151
|
+
frame.title = 'Pixelorama';
|
|
11152
|
+
frame.src = ED.url;
|
|
11153
|
+
const loading = el('div', 'loading');
|
|
11154
|
+
const loadText = el('div');
|
|
11155
|
+
loadText.append(el('b', null, 'Loading the editor'), document.createTextNode('The first open compiles a 40 MB build; later opens come from the browser cache.'));
|
|
11156
|
+
loading.append(loadText);
|
|
11157
|
+
stage.append(frame, loading);
|
|
11158
|
+
panel.append(bar, stage);
|
|
11159
|
+
host.append(scrim, panel);
|
|
11160
|
+
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false };
|
|
11161
|
+
sheetStatus('cool', 'loading');
|
|
11162
|
+
sheetButtons();
|
|
11163
|
+
close.focus({ preventScroll: true });
|
|
11164
|
+
}
|
|
11165
|
+
// The edit file is what the editor gets; when a browser save kept the layered
|
|
11166
|
+
// project beside it, that goes along too and the editor restores the layers,
|
|
11167
|
+
// falling back to the flattened PNG if the file cannot be read.
|
|
11168
|
+
async function sendOpen() {
|
|
11169
|
+
const { item, source } = SHEET;
|
|
11170
|
+
const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
|
|
11171
|
+
const projectUrl = source === item.edit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
|
|
11172
|
+
let png, pxo = null;
|
|
11173
|
+
try {
|
|
11174
|
+
const res = await fetch(source.url, { cache: 'no-store' });
|
|
11175
|
+
if (!res.ok) throw new Error(await res.text());
|
|
11176
|
+
png = await res.arrayBuffer();
|
|
11177
|
+
if (projectUrl) {
|
|
11178
|
+
const p = await fetch(projectUrl, { cache: 'no-store' });
|
|
11179
|
+
if (p.ok) pxo = await p.arrayBuffer();
|
|
11180
|
+
}
|
|
11181
|
+
} catch (err) {
|
|
11182
|
+
if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
|
|
11183
|
+
return;
|
|
11184
|
+
}
|
|
11185
|
+
if (!SHEET) return;
|
|
11186
|
+
const request = 'open-' + (++SHEET.requests);
|
|
11187
|
+
SHEET.sentProject = !!pxo;
|
|
11188
|
+
const message = {
|
|
11189
|
+
type: 'pixelkiln:open', request,
|
|
11190
|
+
asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
|
|
11191
|
+
png, palette: style ? style.palette : [],
|
|
11192
|
+
};
|
|
11193
|
+
const transfer = [png];
|
|
11194
|
+
if (pxo) { message.pxo = pxo; transfer.push(pxo); }
|
|
11195
|
+
SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
|
|
11196
|
+
sheetStatus('cool', 'opening');
|
|
11197
|
+
}
|
|
11198
|
+
function requestEditorSave(close) {
|
|
11199
|
+
if (!SHEET || !SHEET.opened || SHEET.pending) return;
|
|
11200
|
+
const request = 'save-' + (++SHEET.requests);
|
|
11201
|
+
SHEET.pending = { request, close };
|
|
11202
|
+
SHEET.msg.textContent = '';
|
|
11203
|
+
sheetStatus('cool', 'saving');
|
|
11204
|
+
sheetButtons();
|
|
11205
|
+
SHEET.frame.contentWindow.postMessage({ type: 'pixelkiln:request-save', request }, location.origin);
|
|
11206
|
+
}
|
|
11207
|
+
async function onEditorMessage(m) {
|
|
11208
|
+
switch (m.type) {
|
|
11209
|
+
case 'pixelkiln:ready':
|
|
11210
|
+
SHEET.ready = m;
|
|
11211
|
+
if (m.version !== ED.protocol) {
|
|
11212
|
+
SHEET.msg.textContent = 'The editor speaks protocol ' + m.version + '; this gallery expects ' + ED.protocol + '. Reinstall it with pixelkiln tools install editor.';
|
|
11213
|
+
sheetStatus('bad', 'mismatch');
|
|
11214
|
+
return;
|
|
11215
|
+
}
|
|
11216
|
+
sendOpen();
|
|
11217
|
+
break;
|
|
11218
|
+
case 'pixelkiln:opened':
|
|
11219
|
+
SHEET.opened = true;
|
|
11220
|
+
SHEET.loading.remove();
|
|
11221
|
+
if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file with its ' + (m.layers === 1 ? 'layer' : m.layers + ' layers') + ' restored');
|
|
11222
|
+
else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file; its layer file could not be opened');
|
|
11223
|
+
else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
|
|
11224
|
+
sheetButtons();
|
|
11225
|
+
break;
|
|
11226
|
+
case 'pixelkiln:dirty':
|
|
11227
|
+
SHEET.dirty = !!m.dirty;
|
|
11228
|
+
if (SHEET.opened && !SHEET.pending) sheetStatus(SHEET.dirty ? 'warn' : 'ok', SHEET.dirty ? 'unsaved changes' : 'saved');
|
|
11229
|
+
sheetButtons();
|
|
11230
|
+
break;
|
|
11231
|
+
case 'pixelkiln:save': {
|
|
11232
|
+
const pending = SHEET.pending;
|
|
11233
|
+
if (!pending || pending.request !== m.request) return;
|
|
11234
|
+
try {
|
|
11235
|
+
const body = { png: await toBase64(m.png), editor: SHEET.ready.editor, protocol: SHEET.ready.version };
|
|
11236
|
+
if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
|
|
11237
|
+
await postHandEdit(SHEET.item, 'save-edit', body);
|
|
11238
|
+
const saved = snap.items.find((i) => i.id === SHEET.item.id);
|
|
11239
|
+
if (saved) SHEET.item = saved;
|
|
11240
|
+
SHEET.dirty = false;
|
|
11241
|
+
SHEET.pending = null;
|
|
11242
|
+
ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place it in place of the generated art.' };
|
|
11243
|
+
render();
|
|
11244
|
+
if (pending.close) { closeEditorSheet(true); return; }
|
|
11245
|
+
sheetStatus('ok', 'saved');
|
|
11246
|
+
} catch (err) {
|
|
11247
|
+
SHEET.pending = null;
|
|
11248
|
+
SHEET.msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh, then save again.' : '');
|
|
11249
|
+
sheetStatus('bad', 'not saved');
|
|
11250
|
+
}
|
|
11251
|
+
sheetButtons();
|
|
11252
|
+
break;
|
|
11253
|
+
}
|
|
11254
|
+
case 'pixelkiln:error':
|
|
11255
|
+
SHEET.msg.textContent = m.message || 'The editor reported an error.';
|
|
11256
|
+
if (SHEET.pending) { SHEET.pending = null; sheetStatus('bad', 'not saved'); sheetButtons(); }
|
|
11257
|
+
break;
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
function closeEditorSheet(force) {
|
|
11261
|
+
if (!SHEET) return;
|
|
11262
|
+
if (SHEET.dirty && !force && !confirm('Discard unsaved changes in the editor?')) return;
|
|
11263
|
+
SHEET = null;
|
|
11264
|
+
$('dialog-host').textContent = '';
|
|
11265
|
+
refresh();
|
|
11266
|
+
}
|
|
11267
|
+
window.addEventListener('beforeunload', (e) => {
|
|
11268
|
+
if (SHEET && SHEET.dirty) { e.preventDefault(); e.returnValue = ''; }
|
|
10854
11269
|
});
|
|
10855
11270
|
|
|
10856
11271
|
function visibleItems() {
|
|
@@ -11426,6 +11841,11 @@ function handEditSection(item) {
|
|
|
11426
11841
|
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
11427
11842
|
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
11428
11843
|
row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
|
|
11844
|
+
if (item.editMeta) {
|
|
11845
|
+
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 the file changed since' : ''));
|
|
11846
|
+
if (item.editMeta.basedOn) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
11847
|
+
if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
|
|
11848
|
+
}
|
|
11429
11849
|
s.append(dl);
|
|
11430
11850
|
} else {
|
|
11431
11851
|
s.append(el('div', 'state-dim', 'Touch it up in your own editor. The generated file stays untouched; the edit is a sibling file the manifest points at, and mount and pack place that instead.'));
|
|
@@ -11434,7 +11854,7 @@ function handEditSection(item) {
|
|
|
11434
11854
|
const acts = el('div', 'hand-actions');
|
|
11435
11855
|
const msg = el('span', 'msg');
|
|
11436
11856
|
const go = async (label, action, extra, done) => {
|
|
11437
|
-
const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
|
|
11857
|
+
const b = el('button', label === 'Detach edit' || (EDITOR && ED && ED.installed) ? null : 'primary', label); b.type = 'button';
|
|
11438
11858
|
b.onclick = async () => {
|
|
11439
11859
|
b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
|
|
11440
11860
|
try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
|
|
@@ -11442,8 +11862,20 @@ function handEditSection(item) {
|
|
|
11442
11862
|
};
|
|
11443
11863
|
acts.append(b);
|
|
11444
11864
|
};
|
|
11865
|
+
if (EDITOR && ED && (item.edit || canStart) && editorSource(item)) {
|
|
11866
|
+
if (ED.installed) {
|
|
11867
|
+
const b = el('button', 'primary', 'Edit in browser'); b.type = 'button';
|
|
11868
|
+
b.title = 'Open in Pixelorama here; Save writes the edit file and declares it';
|
|
11869
|
+
b.onclick = () => openEditorSheet(item);
|
|
11870
|
+
acts.append(b);
|
|
11871
|
+
} else if (ED.release && !ED.installing) {
|
|
11872
|
+
const b = el('button', null, 'Install editor to edit in browser'); b.type = 'button';
|
|
11873
|
+
b.onclick = () => { installEditor(); window.scrollTo({ top: 0 }); };
|
|
11874
|
+
acts.append(b);
|
|
11875
|
+
}
|
|
11876
|
+
}
|
|
11445
11877
|
if (item.edit) {
|
|
11446
|
-
go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
11878
|
+
go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
11447
11879
|
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
|
|
11448
11880
|
} else if (canStart) {
|
|
11449
11881
|
go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file and opened it in your editor. Save there, then Refresh to see it here.');
|
|
@@ -12170,6 +12602,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
12170
12602
|
if (e.key === '/' && !typing) { e.preventDefault(); $('q').focus(); return; }
|
|
12171
12603
|
if (e.key === 'Escape') {
|
|
12172
12604
|
if (typing && document.activeElement.id === 'q') { document.activeElement.blur(); return; }
|
|
12605
|
+
if (SHEET) { e.preventDefault(); closeEditorSheet(); return; }
|
|
12173
12606
|
if ($('dialog-host').childNodes.length) { e.preventDefault(); $('dialog-host').textContent = ''; return; }
|
|
12174
12607
|
if (ui.open) { e.preventDefault(); closeItem(); }
|
|
12175
12608
|
return;
|
|
@@ -12191,6 +12624,7 @@ ui.compare = ui.compare.filter((id) => snap.items.some((i) => i.id === id));
|
|
|
12191
12624
|
render();
|
|
12192
12625
|
if (ui.open) document.querySelector('.card.active')?.scrollIntoView({ block: 'center' });
|
|
12193
12626
|
if (GENERATION) pollJobs();
|
|
12627
|
+
if (EDITOR) pollEditor();
|
|
12194
12628
|
if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
12195
12629
|
</script>
|
|
12196
12630
|
</body>
|
|
@@ -12203,7 +12637,16 @@ var import_node_child_process4 = require("child_process");
|
|
|
12203
12637
|
var import_node_crypto7 = require("crypto");
|
|
12204
12638
|
var import_node_http2 = require("http");
|
|
12205
12639
|
var import_promises21 = require("fs/promises");
|
|
12206
|
-
var MAX_EDIT_BYTES =
|
|
12640
|
+
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
12641
|
+
var EDITOR_CSP = [
|
|
12642
|
+
"default-src 'self'",
|
|
12643
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'",
|
|
12644
|
+
"style-src 'self' 'unsafe-inline'",
|
|
12645
|
+
"img-src 'self' data: blob:",
|
|
12646
|
+
"worker-src 'self' blob:",
|
|
12647
|
+
"connect-src 'self'",
|
|
12648
|
+
"frame-ancestors 'self'"
|
|
12649
|
+
].join("; ");
|
|
12207
12650
|
async function readJsonBody(req) {
|
|
12208
12651
|
const chunks = [];
|
|
12209
12652
|
let bytes = 0;
|
|
@@ -12221,7 +12664,7 @@ async function readJsonBody(req) {
|
|
|
12221
12664
|
async function serveGallery(opts) {
|
|
12222
12665
|
const log = opts.onProgress ?? (() => {
|
|
12223
12666
|
});
|
|
12224
|
-
const session = opts.edit || opts.generate ? (0, import_node_crypto7.randomBytes)(16).toString("hex") : null;
|
|
12667
|
+
const session = opts.edit || opts.generate || opts.editor ? (0, import_node_crypto7.randomBytes)(16).toString("hex") : null;
|
|
12225
12668
|
let media = /* @__PURE__ */ new Map();
|
|
12226
12669
|
let loading = null;
|
|
12227
12670
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -12296,6 +12739,17 @@ async function serveGallery(opts) {
|
|
|
12296
12739
|
}
|
|
12297
12740
|
return;
|
|
12298
12741
|
}
|
|
12742
|
+
if (req.method === "POST" && url2.pathname === "/api/editor/install") {
|
|
12743
|
+
if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
|
|
12744
|
+
try {
|
|
12745
|
+
const body = await guardedBody("editor installs");
|
|
12746
|
+
if (body === void 0) return;
|
|
12747
|
+
json(202, await opts.editor.install());
|
|
12748
|
+
} catch (err) {
|
|
12749
|
+
reportError("editor install", err);
|
|
12750
|
+
}
|
|
12751
|
+
return;
|
|
12752
|
+
}
|
|
12299
12753
|
const reviewMatch = /^\/review\/([0-9a-f]{16})(\/.*)?$/.exec(url2.pathname);
|
|
12300
12754
|
if (reviewMatch && opts.generate) {
|
|
12301
12755
|
const [, jobId, rest = ""] = reviewMatch;
|
|
@@ -12359,6 +12813,32 @@ async function serveGallery(opts) {
|
|
|
12359
12813
|
if (!opts.generate) return fail(404, "generation is off");
|
|
12360
12814
|
return json(200, opts.generate.status());
|
|
12361
12815
|
}
|
|
12816
|
+
if (url2.pathname === "/api/editor") {
|
|
12817
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
12818
|
+
try {
|
|
12819
|
+
return json(200, await opts.editor.status());
|
|
12820
|
+
} catch (err) {
|
|
12821
|
+
return reportError("editor status", err);
|
|
12822
|
+
}
|
|
12823
|
+
}
|
|
12824
|
+
const editorMatch = /^\/editor\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/.exec(url2.pathname);
|
|
12825
|
+
if (editorMatch) {
|
|
12826
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
12827
|
+
const file = await opts.editor.file(editorMatch[1], editorMatch[2]);
|
|
12828
|
+
if (!file) return fail(404, "that editor file is not installed; check /api/editor");
|
|
12829
|
+
res.writeHead(200, {
|
|
12830
|
+
"Content-Type": file.contentType,
|
|
12831
|
+
"Content-Length": file.bytes.length,
|
|
12832
|
+
// The release tag is in the path, so these bytes never change under
|
|
12833
|
+
// this URL; the 40 MB wasm should come from the browser cache.
|
|
12834
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
12835
|
+
"X-Content-Type-Options": "nosniff",
|
|
12836
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
12837
|
+
...editorMatch[2] === "index.html" ? { "Content-Security-Policy": EDITOR_CSP } : {}
|
|
12838
|
+
});
|
|
12839
|
+
res.end(req.method === "HEAD" ? void 0 : file.bytes);
|
|
12840
|
+
return;
|
|
12841
|
+
}
|
|
12362
12842
|
try {
|
|
12363
12843
|
if (url2.pathname === "/") {
|
|
12364
12844
|
const { snapshot } = await load();
|
|
@@ -12370,7 +12850,8 @@ async function serveGallery(opts) {
|
|
|
12370
12850
|
res.end(renderGallery(snapshot, {
|
|
12371
12851
|
...session ? { session } : {},
|
|
12372
12852
|
editable: Boolean(opts.edit),
|
|
12373
|
-
generation: Boolean(opts.generate)
|
|
12853
|
+
generation: Boolean(opts.generate),
|
|
12854
|
+
editor: Boolean(opts.editor)
|
|
12374
12855
|
}));
|
|
12375
12856
|
return;
|
|
12376
12857
|
}
|
|
@@ -12436,16 +12917,16 @@ async function serveGallery(opts) {
|
|
|
12436
12917
|
|
|
12437
12918
|
// src/gallery/generate.ts
|
|
12438
12919
|
var import_node_crypto8 = require("crypto");
|
|
12439
|
-
var
|
|
12440
|
-
var GenerateRequestSchema =
|
|
12441
|
-
project:
|
|
12442
|
-
keys:
|
|
12920
|
+
var import_zod10 = require("zod");
|
|
12921
|
+
var GenerateRequestSchema = import_zod10.z.object({
|
|
12922
|
+
project: import_zod10.z.string().min(1).optional(),
|
|
12923
|
+
keys: import_zod10.z.array(import_zod10.z.string().min(1)).min(1).max(500),
|
|
12443
12924
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
12444
|
-
force:
|
|
12925
|
+
force: import_zod10.z.boolean().optional(),
|
|
12445
12926
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
12446
|
-
resume:
|
|
12927
|
+
resume: import_zod10.z.boolean().optional(),
|
|
12447
12928
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
12448
|
-
refresh:
|
|
12929
|
+
refresh: import_zod10.z.boolean().optional()
|
|
12449
12930
|
}).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
|
|
12450
12931
|
var GenerateRequestError = class extends Error {
|
|
12451
12932
|
constructor(message6, status = 400) {
|