pixelkiln 0.81.0 → 0.82.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/dist/cli.js +105 -50
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +13 -0
- package/dist/client/gallery.js +270 -7
- package/dist/index.cjs +79 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +74 -19
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +7 -0
- package/docs/CLI.md +7 -0
- package/package.json +1 -1
package/dist/client/gallery.css
CHANGED
|
@@ -525,3 +525,16 @@ body[data-size="l"] .cell { height:210px; }
|
|
|
525
525
|
.keys-sheet dd { margin:0; color:var(--dim); }
|
|
526
526
|
.keys-sheet kbd { font:600 11.5px/1 var(--mono); border:1px solid var(--line-strong); border-bottom-width:2px; padding:2px 6px; }
|
|
527
527
|
@media (max-width: 900px) { .bar { flex-wrap:wrap; } }
|
|
528
|
+
|
|
529
|
+
/* ---- toast, studio template preview ---- */
|
|
530
|
+
.toast { position:fixed; left:22px; bottom:22px; z-index:40; display:flex; align-items:center; gap:10px; max-width:min(560px, calc(100vw - 44px));
|
|
531
|
+
padding:9px 10px 9px 14px; background:var(--panel); border:1px solid var(--line-strong); border-left:3px solid var(--ok);
|
|
532
|
+
box-shadow:0 10px 30px rgba(0,0,0,.45); font-size:13px; }
|
|
533
|
+
.toast[hidden] { display:none; }
|
|
534
|
+
.toast.bad { border-left-color:var(--bad); }
|
|
535
|
+
.toast button { padding:4px 10px; font-size:12.5px; }
|
|
536
|
+
.toast button.x { border:0; color:var(--dim); padding:4px 6px; }
|
|
537
|
+
.studio-tpl-preview { display:flex; align-items:center; gap:10px; }
|
|
538
|
+
.studio-tpl-preview[hidden] { display:none; }
|
|
539
|
+
.studio-tpl-preview img { width:48px; height:48px; image-rendering:pixelated; object-fit:contain; background-color:var(--stage-bg); background-image:var(--stage-pattern); background-size:8px 8px; border:1px solid var(--line); }
|
|
540
|
+
.studio-draft { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
package/dist/client/gallery.js
CHANGED
|
@@ -150,7 +150,15 @@
|
|
|
150
150
|
return s >= 1 ? Math.floor(s) : s;
|
|
151
151
|
};
|
|
152
152
|
var projectOf = (item) => S.snap.workspace ? S.snap.workspace.projects.find((pr) => pr.id === item.project) : S.snap.project;
|
|
153
|
+
var editHooks = { saved: null };
|
|
154
|
+
var manifestShaOf = (snap, project) => snap.workspace ? snap.workspace.projects.find((pr) => pr.id === project)?.manifestSha256 ?? null : snap.project?.manifestSha256 ?? null;
|
|
153
155
|
async function postEdit(body) {
|
|
156
|
+
const before = manifestShaOf(S.snap, body && body.project);
|
|
157
|
+
const snap = await sendEdit(body);
|
|
158
|
+
editHooks.saved?.(body, manifestShaOf(snap, body && body.project) !== before);
|
|
159
|
+
return snap;
|
|
160
|
+
}
|
|
161
|
+
async function sendEdit(body) {
|
|
154
162
|
const res = await fetch("/api/edit", {
|
|
155
163
|
method: "POST",
|
|
156
164
|
headers: { "Content-Type": "application/json", "X-Pixelkiln-Session": SESSION ?? "" },
|
|
@@ -4645,6 +4653,48 @@
|
|
|
4645
4653
|
{ id: "pro", label: "pro: described in words, sequential (20-40 per direction)" }
|
|
4646
4654
|
];
|
|
4647
4655
|
var templated = (mode) => mode === "skeleton-v3" || mode === "template";
|
|
4656
|
+
var KNOWN_TEMPLATES = [
|
|
4657
|
+
"walk",
|
|
4658
|
+
"walking-8-frames",
|
|
4659
|
+
"running-8-frames",
|
|
4660
|
+
"breathing-idle",
|
|
4661
|
+
"crouched-walking",
|
|
4662
|
+
"attack",
|
|
4663
|
+
"attack-back",
|
|
4664
|
+
"attack-left",
|
|
4665
|
+
"attack-right",
|
|
4666
|
+
"cross-punch",
|
|
4667
|
+
"backflip",
|
|
4668
|
+
"angry",
|
|
4669
|
+
"bark"
|
|
4670
|
+
];
|
|
4671
|
+
var draftKey = () => "pixelkiln.studio.draft:" + (S.snap.workspace ? S.snap.workspace.path : S.snap.project ? S.snap.project.manifest : "");
|
|
4672
|
+
function readDraft() {
|
|
4673
|
+
try {
|
|
4674
|
+
const d = JSON.parse(localStorage.getItem(draftKey()) || "null");
|
|
4675
|
+
return d && typeof d === "object" ? d : null;
|
|
4676
|
+
} catch {
|
|
4677
|
+
return null;
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
function writeDraft(draft) {
|
|
4681
|
+
try {
|
|
4682
|
+
if (draft) localStorage.setItem(draftKey(), JSON.stringify(draft));
|
|
4683
|
+
else localStorage.removeItem(draftKey());
|
|
4684
|
+
} catch {
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
function usedTemplates(projectId) {
|
|
4688
|
+
const out = /* @__PURE__ */ new Map();
|
|
4689
|
+
for (const i of S.snap.items) {
|
|
4690
|
+
const t = i.asset && i.asset.animation && i.asset.animation.template;
|
|
4691
|
+
if (!t || i.project !== projectId) continue;
|
|
4692
|
+
const frames = i.outputs.filter((o) => o.url);
|
|
4693
|
+
if (frames.length > 1 && !out.get(t)) out.set(t, i);
|
|
4694
|
+
else if (!out.has(t)) out.set(t, null);
|
|
4695
|
+
}
|
|
4696
|
+
return out;
|
|
4697
|
+
}
|
|
4648
4698
|
var idPattern = "[^\\/\\\\:]+";
|
|
4649
4699
|
function openStudio() {
|
|
4650
4700
|
const host = $("dialog-host");
|
|
@@ -4657,10 +4707,12 @@
|
|
|
4657
4707
|
const scrim = el("div", "sheet-scrim");
|
|
4658
4708
|
const panel = el("div", "sheet studio");
|
|
4659
4709
|
let closed = false;
|
|
4710
|
+
const previewTimers = /* @__PURE__ */ new Set();
|
|
4660
4711
|
const close = () => {
|
|
4661
4712
|
closed = true;
|
|
4662
4713
|
if (priceTimer) clearTimeout(priceTimer);
|
|
4663
4714
|
priceSeq++;
|
|
4715
|
+
for (const t of previewTimers) clearInterval(t);
|
|
4664
4716
|
host.textContent = "";
|
|
4665
4717
|
};
|
|
4666
4718
|
const bar = el("div", "rbar");
|
|
@@ -4734,7 +4786,9 @@
|
|
|
4734
4786
|
const loopList = el("div", "studio-loops");
|
|
4735
4787
|
const addLoop = el("button", "add", "+ Add a loop");
|
|
4736
4788
|
addLoop.type = "button";
|
|
4737
|
-
|
|
4789
|
+
const templateList = el("datalist");
|
|
4790
|
+
templateList.id = "studio-templates";
|
|
4791
|
+
loopsSection.append(el("h3", null, "Loops"), el("small", "state-dim", "Each loop is drawn once per ticked direction; a direction whose mirror is ticked is flipped from it for free."), loopList, addLoop, templateList);
|
|
4738
4792
|
const portrait = el("input");
|
|
4739
4793
|
portrait.type = "checkbox";
|
|
4740
4794
|
const portraitSize = el("select");
|
|
@@ -4805,7 +4859,54 @@
|
|
|
4805
4859
|
function syncLoopMode(row2) {
|
|
4806
4860
|
const t = templated(row2.mode.value);
|
|
4807
4861
|
row2.what.placeholder = t ? "template, e.g. walk, breathing-idle, running-8-frames" : 'the motion, e.g. "swings a sword overhead"';
|
|
4808
|
-
row2.what.title = t ? "A PixelLab template id" : "What the loop does";
|
|
4862
|
+
row2.what.title = t ? "A PixelLab template id: pick one, or type any id PixelLab has" : "What the loop does";
|
|
4863
|
+
if (t) row2.what.setAttribute("list", templateList.id);
|
|
4864
|
+
else row2.what.removeAttribute("list");
|
|
4865
|
+
syncPreview(row2);
|
|
4866
|
+
}
|
|
4867
|
+
function syncPreview(row2) {
|
|
4868
|
+
const old = row2.preview.__timer;
|
|
4869
|
+
if (old) {
|
|
4870
|
+
clearInterval(old);
|
|
4871
|
+
previewTimers.delete(old);
|
|
4872
|
+
}
|
|
4873
|
+
row2.preview.textContent = "";
|
|
4874
|
+
const name = row2.what.value.trim();
|
|
4875
|
+
const sample = templated(row2.mode.value) && name ? usedTemplates(projectId()).get(name) : null;
|
|
4876
|
+
if (!sample) {
|
|
4877
|
+
row2.preview.hidden = true;
|
|
4878
|
+
return;
|
|
4879
|
+
}
|
|
4880
|
+
const urls = sample.outputs.filter((o) => o.url).map((o) => o.url);
|
|
4881
|
+
const img = el("img");
|
|
4882
|
+
img.src = urls[0];
|
|
4883
|
+
img.alt = name;
|
|
4884
|
+
let i = 0;
|
|
4885
|
+
const timer = setInterval(() => {
|
|
4886
|
+
if (!img.isConnected) {
|
|
4887
|
+
clearInterval(timer);
|
|
4888
|
+
previewTimers.delete(timer);
|
|
4889
|
+
return;
|
|
4890
|
+
}
|
|
4891
|
+
if (!document.hidden) {
|
|
4892
|
+
i = (i + 1) % urls.length;
|
|
4893
|
+
img.src = urls[i];
|
|
4894
|
+
}
|
|
4895
|
+
}, Math.round(1e3 / (sample.fps || 12)));
|
|
4896
|
+
row2.preview.__timer = timer;
|
|
4897
|
+
previewTimers.add(timer);
|
|
4898
|
+
row2.preview.append(img, el("small", "state-dim", name + ", as drawn for " + sample.assetId));
|
|
4899
|
+
row2.preview.hidden = false;
|
|
4900
|
+
}
|
|
4901
|
+
function fillTemplateList() {
|
|
4902
|
+
templateList.textContent = "";
|
|
4903
|
+
const used = usedTemplates(projectId());
|
|
4904
|
+
for (const t of [.../* @__PURE__ */ new Set([...used.keys(), ...KNOWN_TEMPLATES])]) {
|
|
4905
|
+
const o = el("option");
|
|
4906
|
+
o.value = t;
|
|
4907
|
+
if (used.has(t)) o.label = "used in this project";
|
|
4908
|
+
templateList.append(o);
|
|
4909
|
+
}
|
|
4809
4910
|
}
|
|
4810
4911
|
function addLoopRow(defaults = {}) {
|
|
4811
4912
|
const wrap = el("div", "studio-loop");
|
|
@@ -4835,11 +4936,15 @@
|
|
|
4835
4936
|
}
|
|
4836
4937
|
const remove = el("button", null, "Remove");
|
|
4837
4938
|
remove.type = "button";
|
|
4838
|
-
const
|
|
4939
|
+
const preview = el("div", "studio-tpl-preview");
|
|
4940
|
+
preview.hidden = true;
|
|
4941
|
+
const row2 = { id, mode, what, directions: boxes, wrap, preview };
|
|
4942
|
+
what.addEventListener("input", () => syncPreview(row2));
|
|
4839
4943
|
remove.onclick = () => {
|
|
4840
4944
|
loops2.splice(loops2.indexOf(row2), 1);
|
|
4841
4945
|
wrap.remove();
|
|
4842
4946
|
schedulePrice();
|
|
4947
|
+
keepDraft();
|
|
4843
4948
|
};
|
|
4844
4949
|
mode.onchange = () => {
|
|
4845
4950
|
syncLoopMode(row2);
|
|
@@ -4847,7 +4952,7 @@
|
|
|
4847
4952
|
};
|
|
4848
4953
|
const line = el("div", "row");
|
|
4849
4954
|
line.append(field("loop id", id), field("mode", mode), field("template or motion", what));
|
|
4850
|
-
wrap.append(line, dirs, remove);
|
|
4955
|
+
wrap.append(line, preview, dirs, remove);
|
|
4851
4956
|
loopList.append(wrap);
|
|
4852
4957
|
loops2.push(row2);
|
|
4853
4958
|
syncLoopMode(row2);
|
|
@@ -4985,6 +5090,7 @@
|
|
|
4985
5090
|
const keys = price ? price.items.filter((i) => ours(i.key)).map((i) => i.key) : [];
|
|
4986
5091
|
try {
|
|
4987
5092
|
S.snap = await postEdit(request());
|
|
5093
|
+
writeDraft(null);
|
|
4988
5094
|
const newId = (pid ? pid + ":" : "") + sid + "/" + base;
|
|
4989
5095
|
if (generate && keys.length) {
|
|
4990
5096
|
await postGenerate({ keys, ...pid ? { project: pid } : {} });
|
|
@@ -5078,11 +5184,84 @@
|
|
|
5078
5184
|
};
|
|
5079
5185
|
form.addEventListener("input", schedulePrice);
|
|
5080
5186
|
form.addEventListener("change", schedulePrice);
|
|
5187
|
+
const capture = () => ({
|
|
5188
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5189
|
+
project: project.value,
|
|
5190
|
+
style: style.value,
|
|
5191
|
+
styleId: styleId.value,
|
|
5192
|
+
engine: engine.value,
|
|
5193
|
+
size: size.value,
|
|
5194
|
+
directions: directions.value,
|
|
5195
|
+
body: body.value,
|
|
5196
|
+
view: view.value,
|
|
5197
|
+
outDir: outDir.value,
|
|
5198
|
+
outDirTouched,
|
|
5199
|
+
baseId: baseId.value,
|
|
5200
|
+
prompt: prompt.value,
|
|
5201
|
+
reference,
|
|
5202
|
+
loops: loops2.map((r) => ({ id: r.id.value, mode: r.mode.value, what: r.what.value, directions: [...r.directions].filter(([, b]) => b.checked).map(([d]) => d) })),
|
|
5203
|
+
portrait: portrait.checked,
|
|
5204
|
+
portraitSize: portraitSize.value
|
|
5205
|
+
});
|
|
5206
|
+
let draftTimer = null;
|
|
5207
|
+
const keepDraft = () => {
|
|
5208
|
+
if (closed) return;
|
|
5209
|
+
if (draftTimer) clearTimeout(draftTimer);
|
|
5210
|
+
draftTimer = setTimeout(() => {
|
|
5211
|
+
if (!closed) writeDraft(baseId.value.trim() || prompt.value.trim() ? capture() : null);
|
|
5212
|
+
}, 300);
|
|
5213
|
+
};
|
|
5214
|
+
form.addEventListener("input", keepDraft);
|
|
5215
|
+
form.addEventListener("change", keepDraft);
|
|
5216
|
+
addLoop.addEventListener("click", keepDraft);
|
|
5081
5217
|
fillStyles();
|
|
5218
|
+
fillTemplateList();
|
|
5082
5219
|
syncEngine();
|
|
5083
5220
|
syncStyle();
|
|
5084
|
-
|
|
5085
|
-
|
|
5221
|
+
const draft = readDraft();
|
|
5222
|
+
if (draft && (draft.baseId || draft.prompt)) {
|
|
5223
|
+
const set = (input, value) => {
|
|
5224
|
+
if (typeof value !== "string") return;
|
|
5225
|
+
if (input instanceof HTMLSelectElement && ![...input.options].some((o) => o.value === value)) return;
|
|
5226
|
+
input.value = value;
|
|
5227
|
+
};
|
|
5228
|
+
set(project, draft.project);
|
|
5229
|
+
fillStyles();
|
|
5230
|
+
set(style, draft.style);
|
|
5231
|
+
set(engine, draft.engine);
|
|
5232
|
+
syncEngine();
|
|
5233
|
+
set(styleId, draft.styleId);
|
|
5234
|
+
set(size, draft.size);
|
|
5235
|
+
set(directions, draft.directions);
|
|
5236
|
+
set(body, draft.body);
|
|
5237
|
+
set(view, draft.view);
|
|
5238
|
+
outDirTouched = Boolean(draft.outDirTouched);
|
|
5239
|
+
set(outDir, draft.outDir);
|
|
5240
|
+
set(baseId, draft.baseId);
|
|
5241
|
+
set(prompt, draft.prompt);
|
|
5242
|
+
if (typeof draft.reference === "string") {
|
|
5243
|
+
reference = draft.reference;
|
|
5244
|
+
refField.append(el("small", "state-dim studio-ref-kept", "Using " + draft.reference + ", uploaded with this draft. Choose a file to replace it."));
|
|
5245
|
+
}
|
|
5246
|
+
portrait.checked = Boolean(draft.portrait);
|
|
5247
|
+
set(portraitSize, draft.portraitSize);
|
|
5248
|
+
syncStyle();
|
|
5249
|
+
for (const l of Array.isArray(draft.loops) ? draft.loops : []) addLoopRow({ id: l.id, mode: l.mode, what: l.what, directions: l.directions });
|
|
5250
|
+
const note = el("div", "notice studio-draft");
|
|
5251
|
+
note.append(document.createTextNode("Picked up the draft you left " + (fmtWhen(draft.savedAt) || "earlier") + ". "));
|
|
5252
|
+
const discard = el("button", "linkish", "Start over");
|
|
5253
|
+
discard.type = "button";
|
|
5254
|
+
discard.onclick = () => {
|
|
5255
|
+
writeDraft(null);
|
|
5256
|
+
close();
|
|
5257
|
+
openStudio();
|
|
5258
|
+
};
|
|
5259
|
+
note.append(discard);
|
|
5260
|
+
form.prepend(note);
|
|
5261
|
+
} else {
|
|
5262
|
+
addLoopRow({ id: "walk", mode: "skeleton-v3", what: "walk", directions: ["south", "west", "north"] });
|
|
5263
|
+
addLoopRow({ id: "idle", mode: "skeleton-v3", what: "breathing-idle", directions: ["south"] });
|
|
5264
|
+
}
|
|
5086
5265
|
updatePrice();
|
|
5087
5266
|
setTimeout(() => baseId.focus(), 0);
|
|
5088
5267
|
}
|
|
@@ -5094,7 +5273,8 @@
|
|
|
5094
5273
|
keys: [
|
|
5095
5274
|
[["/"], "jump to search"],
|
|
5096
5275
|
[["?"], "this list"],
|
|
5097
|
-
[["Esc"], "close whatever is open: a sheet, the record, the View menu, or select mode"]
|
|
5276
|
+
[["Esc"], "close whatever is open: a sheet, the record, the View menu, or select mode"],
|
|
5277
|
+
[["⌘/ctrl", "Z"], "undo the gallery's last manifest edit (with --edit)"]
|
|
5098
5278
|
]
|
|
5099
5279
|
},
|
|
5100
5280
|
{
|
|
@@ -5162,8 +5342,86 @@
|
|
|
5162
5342
|
close.focus();
|
|
5163
5343
|
}
|
|
5164
5344
|
|
|
5345
|
+
// src/gallery/client/src/undo.ts
|
|
5346
|
+
var lastProject = null;
|
|
5347
|
+
var hideTimer = null;
|
|
5348
|
+
function describe(body) {
|
|
5349
|
+
switch (body.action) {
|
|
5350
|
+
case "batch":
|
|
5351
|
+
return body.edits.length + " changes to the manifest";
|
|
5352
|
+
case "add-style":
|
|
5353
|
+
return "added style " + body.styleId;
|
|
5354
|
+
case "patch-style":
|
|
5355
|
+
return "changed style " + body.styleId;
|
|
5356
|
+
case "add-asset":
|
|
5357
|
+
return "added " + body.assetId;
|
|
5358
|
+
case "patch-asset":
|
|
5359
|
+
return "changed " + body.assetId;
|
|
5360
|
+
case "set-source":
|
|
5361
|
+
case "clear-source":
|
|
5362
|
+
return "changed the source of " + body.assetId;
|
|
5363
|
+
case "create-skeleton-animation":
|
|
5364
|
+
return "added " + body.assetId;
|
|
5365
|
+
case "start-edit":
|
|
5366
|
+
return "started a hand edit of " + body.assetId;
|
|
5367
|
+
case "save-edit":
|
|
5368
|
+
return "saved a hand edit of " + body.assetId;
|
|
5369
|
+
case "detach-edit":
|
|
5370
|
+
return "detached the hand edit of " + body.assetId;
|
|
5371
|
+
default:
|
|
5372
|
+
return "the manifest changed";
|
|
5373
|
+
}
|
|
5374
|
+
}
|
|
5375
|
+
function toast(text, canUndo, tone = "") {
|
|
5376
|
+
const host = $("toast");
|
|
5377
|
+
host.textContent = "";
|
|
5378
|
+
host.className = "toast" + (tone ? " " + tone : "");
|
|
5379
|
+
host.append(el("span", null, text));
|
|
5380
|
+
if (canUndo) {
|
|
5381
|
+
const b = el("button", null, "Undo");
|
|
5382
|
+
b.type = "button";
|
|
5383
|
+
b.title = "Put the manifest back as it was (⌘Z / ctrl+Z)";
|
|
5384
|
+
b.onclick = () => void undoLast();
|
|
5385
|
+
host.append(b);
|
|
5386
|
+
}
|
|
5387
|
+
const x = el("button", "x", "×");
|
|
5388
|
+
x.type = "button";
|
|
5389
|
+
x.setAttribute("aria-label", "Dismiss");
|
|
5390
|
+
x.onclick = () => {
|
|
5391
|
+
host.hidden = true;
|
|
5392
|
+
};
|
|
5393
|
+
host.append(x);
|
|
5394
|
+
host.hidden = false;
|
|
5395
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
5396
|
+
hideTimer = setTimeout(() => {
|
|
5397
|
+
host.hidden = true;
|
|
5398
|
+
}, 12e3);
|
|
5399
|
+
}
|
|
5400
|
+
function installUndo() {
|
|
5401
|
+
editHooks.saved = (body, changed) => {
|
|
5402
|
+
if (!body || body.action === "undo-edit" || !changed) return;
|
|
5403
|
+
lastProject = body.project ?? null;
|
|
5404
|
+
toast("Saved: " + describe(body) + ".", true);
|
|
5405
|
+
};
|
|
5406
|
+
}
|
|
5407
|
+
async function undoLast() {
|
|
5408
|
+
if (!EDITABLE) return;
|
|
5409
|
+
const expected = manifestShaOf(S.snap, lastProject);
|
|
5410
|
+
if (!expected) return;
|
|
5411
|
+
const body = { action: "undo-edit", expectedSha256: expected };
|
|
5412
|
+
if (lastProject) body.project = lastProject;
|
|
5413
|
+
try {
|
|
5414
|
+
S.snap = await postEdit(body);
|
|
5415
|
+
render();
|
|
5416
|
+
toast("Undone. The manifest is back as it was before that edit.", true);
|
|
5417
|
+
} catch (err) {
|
|
5418
|
+
toast(/nothing to undo/.test(err.message) ? "Nothing left to undo." : "Could not undo: " + err.message, false, "bad");
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
|
|
5165
5422
|
// src/gallery/client/src/main.ts
|
|
5166
5423
|
loadPrefs();
|
|
5424
|
+
installUndo();
|
|
5167
5425
|
$("refresh").onclick = refresh;
|
|
5168
5426
|
$("select").onclick = () => setSelecting(!ui.selecting);
|
|
5169
5427
|
$("keys").onclick = openShortcuts;
|
|
@@ -5234,6 +5492,11 @@
|
|
|
5234
5492
|
openShortcuts();
|
|
5235
5493
|
return;
|
|
5236
5494
|
}
|
|
5495
|
+
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z" && !typing && EDITABLE && !$("dialog-host").childNodes.length) {
|
|
5496
|
+
e.preventDefault();
|
|
5497
|
+
void undoLast();
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5237
5500
|
if (e.key === "Escape") {
|
|
5238
5501
|
if ($("view-menu").open) {
|
|
5239
5502
|
e.preventDefault();
|
package/dist/index.cjs
CHANGED
|
@@ -15256,7 +15256,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
15256
15256
|
// src/gallery/snapshot.ts
|
|
15257
15257
|
var import_node_crypto6 = require("crypto");
|
|
15258
15258
|
var import_node_fs27 = require("fs");
|
|
15259
|
-
var
|
|
15259
|
+
var import_promises25 = require("fs/promises");
|
|
15260
15260
|
var import_node_path32 = __toESM(require("path"), 1);
|
|
15261
15261
|
|
|
15262
15262
|
// src/workspace.ts
|
|
@@ -15393,6 +15393,7 @@ function validateWorkspace(ws, dir) {
|
|
|
15393
15393
|
}
|
|
15394
15394
|
|
|
15395
15395
|
// src/gallery/edit.ts
|
|
15396
|
+
var import_promises24 = require("fs/promises");
|
|
15396
15397
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
15397
15398
|
var import_zod12 = require("zod");
|
|
15398
15399
|
|
|
@@ -16248,9 +16249,62 @@ var HandEditRequestSchema = import_zod12.z.discriminatedUnion("action", [
|
|
|
16248
16249
|
protocol: import_zod12.z.number().int().positive()
|
|
16249
16250
|
}).strict()
|
|
16250
16251
|
]);
|
|
16252
|
+
var UNDO_DEPTH = 20;
|
|
16253
|
+
var UndoRequestSchema = import_zod12.z.object({
|
|
16254
|
+
action: import_zod12.z.literal("undo-edit"),
|
|
16255
|
+
project: import_zod12.z.string().min(1).optional(),
|
|
16256
|
+
/** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
|
|
16257
|
+
expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/)
|
|
16258
|
+
}).strict();
|
|
16251
16259
|
function createGalleryEditHandler(opts) {
|
|
16252
16260
|
const log = opts.onProgress ?? (() => {
|
|
16253
16261
|
});
|
|
16262
|
+
const apply = createEditApplier(opts, log);
|
|
16263
|
+
const history = /* @__PURE__ */ new Map();
|
|
16264
|
+
const manifestOf = async (project) => {
|
|
16265
|
+
try {
|
|
16266
|
+
return import_node_path31.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
|
|
16267
|
+
} catch {
|
|
16268
|
+
return null;
|
|
16269
|
+
}
|
|
16270
|
+
};
|
|
16271
|
+
return async (body) => {
|
|
16272
|
+
const undo = UndoRequestSchema.safeParse(body);
|
|
16273
|
+
if (undo.success) {
|
|
16274
|
+
const manifestPath2 = await manifestOf(undo.data.project);
|
|
16275
|
+
if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
|
|
16276
|
+
const current = await (0, import_promises24.readFile)(manifestPath2, "utf8");
|
|
16277
|
+
if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
|
|
16278
|
+
const stack = history.get(manifestPath2) ?? [];
|
|
16279
|
+
const last = stack[stack.length - 1];
|
|
16280
|
+
if (!last) throw new ManifestEditError("nothing to undo: this gallery has not changed the manifest");
|
|
16281
|
+
if (last.afterSha !== sha256(current)) {
|
|
16282
|
+
history.delete(manifestPath2);
|
|
16283
|
+
throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
|
|
16284
|
+
}
|
|
16285
|
+
const tmp = manifestPath2 + ".pixelkiln-undo";
|
|
16286
|
+
await (0, import_promises24.writeFile)(tmp, last.before);
|
|
16287
|
+
await (0, import_promises24.rename)(tmp, manifestPath2);
|
|
16288
|
+
stack.pop();
|
|
16289
|
+
log(` manifest edit undone in ${import_node_path31.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
|
|
16290
|
+
return opts.reload();
|
|
16291
|
+
}
|
|
16292
|
+
const manifestPath = await manifestOf(body?.project);
|
|
16293
|
+
const before = manifestPath ? await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null) : null;
|
|
16294
|
+
const build = await apply(body);
|
|
16295
|
+
if (manifestPath && before !== null) {
|
|
16296
|
+
const after = await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null);
|
|
16297
|
+
if (after !== null && after !== before) {
|
|
16298
|
+
const stack = history.get(manifestPath) ?? [];
|
|
16299
|
+
stack.push({ before, afterSha: sha256(after) });
|
|
16300
|
+
if (stack.length > UNDO_DEPTH) stack.shift();
|
|
16301
|
+
history.set(manifestPath, stack);
|
|
16302
|
+
}
|
|
16303
|
+
}
|
|
16304
|
+
return build;
|
|
16305
|
+
};
|
|
16306
|
+
}
|
|
16307
|
+
function createEditApplier(opts, log) {
|
|
16254
16308
|
return async (body) => {
|
|
16255
16309
|
const handEdit = HandEditRequestSchema.safeParse(body);
|
|
16256
16310
|
if (handEdit.success) {
|
|
@@ -16425,7 +16479,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
16425
16479
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
16426
16480
|
async function samePixels(a, b) {
|
|
16427
16481
|
try {
|
|
16428
|
-
const [left, right] = await Promise.all([(0,
|
|
16482
|
+
const [left, right] = await Promise.all([(0, import_promises25.readFile)(a), (0, import_promises25.readFile)(b)]);
|
|
16429
16483
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
16430
16484
|
const x = decodePng(left);
|
|
16431
16485
|
const y = decodePng(right);
|
|
@@ -16491,7 +16545,7 @@ function describeCharacter(spec, entry) {
|
|
|
16491
16545
|
}
|
|
16492
16546
|
async function fileInfo(absolutePath) {
|
|
16493
16547
|
try {
|
|
16494
|
-
const info = await (0,
|
|
16548
|
+
const info = await (0, import_promises25.stat)(absolutePath);
|
|
16495
16549
|
return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
|
|
16496
16550
|
} catch {
|
|
16497
16551
|
return null;
|
|
@@ -16583,7 +16637,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16583
16637
|
const media = /* @__PURE__ */ new Map();
|
|
16584
16638
|
const plan = await buildPlan(specs, lock);
|
|
16585
16639
|
const items = [];
|
|
16586
|
-
const manifestText = await (0,
|
|
16640
|
+
const manifestText = await (0, import_promises25.readFile)(loaded.path, "utf8");
|
|
16587
16641
|
const manifestSha256 = sha256(manifestText);
|
|
16588
16642
|
const cacheDir = import_node_path32.default.resolve(import_node_path32.default.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
16589
16643
|
let rawStyles = {};
|
|
@@ -16935,7 +16989,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16935
16989
|
root: loaded.root,
|
|
16936
16990
|
provider: loaded.manifest.provider,
|
|
16937
16991
|
account: project.account ?? null,
|
|
16938
|
-
manifestSha256: manifestSha256 ?? sha256(await (0,
|
|
16992
|
+
manifestSha256: manifestSha256 ?? sha256(await (0, import_promises25.readFile)(manifestPath, "utf8")),
|
|
16939
16993
|
historyLimit: historyLimit(loaded.manifest),
|
|
16940
16994
|
entries: Object.keys(lock.entries).length,
|
|
16941
16995
|
items: projectItems.length,
|
|
@@ -17100,6 +17154,7 @@ ${css}</style>
|
|
|
17100
17154
|
</header>
|
|
17101
17155
|
<main id="root"></main>
|
|
17102
17156
|
<div id="selbar" class="selbar" hidden></div>
|
|
17157
|
+
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
|
17103
17158
|
<footer>
|
|
17104
17159
|
Click a sprite for its full record; press <kbd>?</kbd> for every keyboard shortcut. This page reads the manifest, lockfile,
|
|
17105
17160
|
and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
|
|
@@ -17176,7 +17231,7 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
|
|
|
17176
17231
|
// src/editor/install.ts
|
|
17177
17232
|
var import_node_crypto7 = require("crypto");
|
|
17178
17233
|
var import_node_fs29 = require("fs");
|
|
17179
|
-
var
|
|
17234
|
+
var import_promises26 = require("fs/promises");
|
|
17180
17235
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
17181
17236
|
var import_node_path33 = __toESM(require("path"), 1);
|
|
17182
17237
|
var EditorInstallError = class extends Error {
|
|
@@ -17207,7 +17262,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
17207
17262
|
}
|
|
17208
17263
|
async function sha256Of(file) {
|
|
17209
17264
|
try {
|
|
17210
|
-
return (0, import_node_crypto7.createHash)("sha256").update(await (0,
|
|
17265
|
+
return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises26.readFile)(file)).digest("hex");
|
|
17211
17266
|
} catch {
|
|
17212
17267
|
return null;
|
|
17213
17268
|
}
|
|
@@ -17221,7 +17276,7 @@ async function editorStatus(opts = {}) {
|
|
|
17221
17276
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17222
17277
|
totalBytes += expected.bytes;
|
|
17223
17278
|
const file = import_node_path33.default.join(dir, name);
|
|
17224
|
-
const info = (0, import_node_fs29.existsSync)(file) ? await (0,
|
|
17279
|
+
const info = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises26.stat)(file).catch(() => null) : null;
|
|
17225
17280
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17226
17281
|
installedBytes += expected.bytes;
|
|
17227
17282
|
} else {
|
|
@@ -17250,7 +17305,7 @@ async function installEditor(opts = {}) {
|
|
|
17250
17305
|
);
|
|
17251
17306
|
}
|
|
17252
17307
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
17253
|
-
await (0,
|
|
17308
|
+
await (0, import_promises26.mkdir)(dir, { recursive: true });
|
|
17254
17309
|
const downloaded = [];
|
|
17255
17310
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
17256
17311
|
let fetchedBytes = before.installedBytes;
|
|
@@ -17281,10 +17336,10 @@ async function installEditor(opts = {}) {
|
|
|
17281
17336
|
const final = import_node_path33.default.join(dir, name);
|
|
17282
17337
|
const tmp = `${final}.${process.pid}.part`;
|
|
17283
17338
|
try {
|
|
17284
|
-
await (0,
|
|
17285
|
-
await (0,
|
|
17339
|
+
await (0, import_promises26.writeFile)(tmp, bytes);
|
|
17340
|
+
await (0, import_promises26.rename)(tmp, final);
|
|
17286
17341
|
} finally {
|
|
17287
|
-
await (0,
|
|
17342
|
+
await (0, import_promises26.rm)(tmp, { force: true });
|
|
17288
17343
|
}
|
|
17289
17344
|
fetchedBytes += bytes.length;
|
|
17290
17345
|
downloaded.push(name);
|
|
@@ -17328,7 +17383,7 @@ function editorContentType(name) {
|
|
|
17328
17383
|
}
|
|
17329
17384
|
|
|
17330
17385
|
// src/gallery/editor.ts
|
|
17331
|
-
var
|
|
17386
|
+
var import_promises27 = require("fs/promises");
|
|
17332
17387
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
17333
17388
|
function editorRoute(release, name = "index.html") {
|
|
17334
17389
|
return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
|
|
@@ -17402,7 +17457,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17402
17457
|
if (!status.installed) return null;
|
|
17403
17458
|
let bytes;
|
|
17404
17459
|
try {
|
|
17405
|
-
bytes = await (0,
|
|
17460
|
+
bytes = await (0, import_promises27.readFile)(import_node_path34.default.join(dir, name));
|
|
17406
17461
|
} catch {
|
|
17407
17462
|
verified = null;
|
|
17408
17463
|
return null;
|
|
@@ -17419,7 +17474,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17419
17474
|
// src/gallery/server.ts
|
|
17420
17475
|
var import_node_crypto8 = require("crypto");
|
|
17421
17476
|
var import_node_http2 = require("http");
|
|
17422
|
-
var
|
|
17477
|
+
var import_promises28 = require("fs/promises");
|
|
17423
17478
|
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
17424
17479
|
var EDITOR_CSP = [
|
|
17425
17480
|
"default-src 'self'",
|
|
@@ -17600,7 +17655,7 @@ async function serveGallery(opts) {
|
|
|
17600
17655
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
17601
17656
|
if (!asset) return fail(404, "no such review asset");
|
|
17602
17657
|
try {
|
|
17603
|
-
const bytes = await (0,
|
|
17658
|
+
const bytes = await (0, import_promises28.readFile)(asset.path);
|
|
17604
17659
|
res.writeHead(200, {
|
|
17605
17660
|
"Content-Type": asset.contentType,
|
|
17606
17661
|
"Cache-Control": "no-store",
|
|
@@ -17683,7 +17738,7 @@ async function serveGallery(opts) {
|
|
|
17683
17738
|
if (!asset) return fail(404, "no such gallery media");
|
|
17684
17739
|
let bytes;
|
|
17685
17740
|
try {
|
|
17686
|
-
bytes = await (0,
|
|
17741
|
+
bytes = await (0, import_promises28.readFile)(asset.path);
|
|
17687
17742
|
} catch {
|
|
17688
17743
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
17689
17744
|
}
|
|
@@ -18093,7 +18148,7 @@ function createGenerateHandlers(opts) {
|
|
|
18093
18148
|
}
|
|
18094
18149
|
|
|
18095
18150
|
// src/pipeline/salvage.ts
|
|
18096
|
-
var
|
|
18151
|
+
var import_promises29 = require("fs/promises");
|
|
18097
18152
|
var import_node_fs30 = require("fs");
|
|
18098
18153
|
var import_node_path35 = __toESM(require("path"), 1);
|
|
18099
18154
|
function providerClaimId(provider, objectId) {
|
|
@@ -18105,7 +18160,7 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
18105
18160
|
if (!(0, import_node_fs30.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
18106
18161
|
let parsed;
|
|
18107
18162
|
try {
|
|
18108
|
-
parsed = parseLock(JSON.parse(await (0,
|
|
18163
|
+
parsed = parseLock(JSON.parse(await (0, import_promises29.readFile)(p, "utf8")));
|
|
18109
18164
|
} catch {
|
|
18110
18165
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
18111
18166
|
}
|
|
@@ -18342,7 +18397,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
18342
18397
|
}
|
|
18343
18398
|
|
|
18344
18399
|
// src/pick/salvage-server.ts
|
|
18345
|
-
var
|
|
18400
|
+
var import_promises30 = require("fs/promises");
|
|
18346
18401
|
var import_node_path36 = __toESM(require("path"), 1);
|
|
18347
18402
|
|
|
18348
18403
|
// src/pick/salvage-sheet.ts
|
|
@@ -18567,8 +18622,8 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
18567
18622
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
18568
18623
|
const rel = import_node_path36.default.join("_salvaged", `${assetId}.png`);
|
|
18569
18624
|
const outFile = import_node_path36.default.resolve(ctx.importDir, rel);
|
|
18570
|
-
await (0,
|
|
18571
|
-
await (0,
|
|
18625
|
+
await (0, import_promises30.mkdir)(import_node_path36.default.dirname(outFile), { recursive: true });
|
|
18626
|
+
await (0, import_promises30.writeFile)(outFile, buf);
|
|
18572
18627
|
ctx.manifest.assets[assetId] = {
|
|
18573
18628
|
prompt: orphan.prompt,
|
|
18574
18629
|
promptByStyle: {},
|
|
@@ -18624,9 +18679,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
18624
18679
|
}
|
|
18625
18680
|
}
|
|
18626
18681
|
await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
|
|
18627
|
-
const raw = JSON.parse(await (0,
|
|
18682
|
+
const raw = JSON.parse(await (0, import_promises30.readFile)(ctx.manifestPath, "utf8"));
|
|
18628
18683
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
18629
|
-
await (0,
|
|
18684
|
+
await (0, import_promises30.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
18630
18685
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
18631
18686
|
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
18632
18687
|
return result;
|