pixelkiln 0.77.0 → 0.78.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 +5 -0
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +26 -0
- package/dist/client/gallery.js +286 -7
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +20 -0
- package/package.json +1 -1
package/dist/client/gallery.css
CHANGED
|
@@ -440,3 +440,29 @@ td.col-on { background:rgba(255,107,53,.08); }
|
|
|
440
440
|
.frames-bar .count { font:11.5px/1 var(--mono); color:var(--dim); min-width:44px; }
|
|
441
441
|
.frames-bar select { font:11.5px/1 var(--mono); padding:5px 6px; background:var(--panel-deep); border:1px solid var(--line); }
|
|
442
442
|
.frames-bar .chip { font-size:12px; color:var(--dim); display:inline-flex; align-items:center; gap:4px; }
|
|
443
|
+
|
|
444
|
+
/* ---- families on the grid, selection ---- */
|
|
445
|
+
.fam-group { margin:0 0 18px; }
|
|
446
|
+
.fam-group + .fam-group { border-top:1px dashed var(--line); padding-top:14px; }
|
|
447
|
+
.fam-head { display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin:0 0 10px; }
|
|
448
|
+
.fam-head > b { font:650 13px/1.3 var(--mono); }
|
|
449
|
+
.fam-tally { display:inline-flex; flex-wrap:wrap; gap:10px; color:var(--dim); font-size:12px; }
|
|
450
|
+
.fam-tally .st { display:inline-flex; align-items:center; gap:5px; font-family:var(--mono); font-size:11.5px; }
|
|
451
|
+
.fam-head .tools { margin-left:auto; display:flex; gap:6px; flex-wrap:wrap; }
|
|
452
|
+
.fam-head .tools button { font-size:12.5px; padding:5px 10px; }
|
|
453
|
+
#select.on { border-color:var(--accent); color:var(--accent-soft); }
|
|
454
|
+
.card .sel { position:absolute; top:6px; left:6px; width:20px; height:20px; display:grid; place-items:center;
|
|
455
|
+
font:700 12px/1 var(--mono); color:transparent; background:rgba(16,15,12,.75); border:1px solid var(--line-strong); }
|
|
456
|
+
.card.selected .sel { color:var(--bg); background:var(--accent); border-color:var(--accent); }
|
|
457
|
+
.card.selected { outline:2px solid var(--accent); outline-offset:-2px; }
|
|
458
|
+
.selbar { position:sticky; bottom:0; z-index:12; display:flex; flex-wrap:wrap; align-items:center; gap:10px 14px;
|
|
459
|
+
padding:10px 22px; background:var(--panel); border-top:1px solid var(--line-strong); box-shadow:0 -6px 18px rgba(0,0,0,.35); }
|
|
460
|
+
.selbar[hidden] { display:none; }
|
|
461
|
+
.selbar > b { font:650 12.5px/1 var(--mono); }
|
|
462
|
+
.selbar .acts { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
|
|
463
|
+
.selbar .acts button { font-size:12.5px; padding:5px 10px; }
|
|
464
|
+
.selbar .tagbox { display:inline-flex; gap:4px; }
|
|
465
|
+
.selbar .tagbox input { width:120px; padding:5px 8px; background:var(--panel-deep); border:1px solid var(--line); }
|
|
466
|
+
.selbar .msg { font-size:12px; color:var(--dim); }
|
|
467
|
+
.selbar .msg.bad { color:var(--bad); } .selbar .msg.ok { color:var(--ok); }
|
|
468
|
+
@media (max-width: 640px) { .selbar { padding:10px 12px; } }
|
package/dist/client/gallery.js
CHANGED
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
backdrop: "checker",
|
|
45
45
|
notify: false,
|
|
46
46
|
fps: null,
|
|
47
|
-
onion: false
|
|
47
|
+
onion: false,
|
|
48
|
+
selecting: false,
|
|
49
|
+
selected: /* @__PURE__ */ new Set()
|
|
48
50
|
};
|
|
49
51
|
var PREFS_KEY = "pixelkiln.gallery.prefs";
|
|
50
52
|
var PREF_NAMES = ["playLoops", "backdrop", "notify", "onion"];
|
|
@@ -1012,6 +1014,180 @@
|
|
|
1012
1014
|
if (!document.hidden) wake();
|
|
1013
1015
|
});
|
|
1014
1016
|
|
|
1017
|
+
// src/gallery/client/src/selection.ts
|
|
1018
|
+
var selectedItems = () => S.snap.items.filter((i) => ui.selected.has(i.id));
|
|
1019
|
+
var regenerable = (i) => i.declared && i.currentSpecHash !== null && i.state === "ok";
|
|
1020
|
+
function setSelecting(on) {
|
|
1021
|
+
ui.selecting = on;
|
|
1022
|
+
if (!on) ui.selected.clear();
|
|
1023
|
+
$("select").textContent = on ? "Done selecting" : "Select";
|
|
1024
|
+
$("select").classList.toggle("on", on);
|
|
1025
|
+
render();
|
|
1026
|
+
}
|
|
1027
|
+
function toggleSelected(id) {
|
|
1028
|
+
if (!ui.selecting) {
|
|
1029
|
+
ui.selecting = true;
|
|
1030
|
+
$("select").textContent = "Done selecting";
|
|
1031
|
+
$("select").classList.add("on");
|
|
1032
|
+
}
|
|
1033
|
+
if (ui.selected.has(id)) ui.selected.delete(id);
|
|
1034
|
+
else ui.selected.add(id);
|
|
1035
|
+
const c = document.querySelector('.card[data-key="' + CSS.escape(id) + '"]');
|
|
1036
|
+
if (c) {
|
|
1037
|
+
c.classList.toggle("selected", ui.selected.has(id));
|
|
1038
|
+
c.setAttribute("aria-pressed", String(ui.selected.has(id)));
|
|
1039
|
+
}
|
|
1040
|
+
if (!document.querySelector(".card .sel")) render();
|
|
1041
|
+
else {
|
|
1042
|
+
refreshSelectAll();
|
|
1043
|
+
renderSelectionBar();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function selectAllButton(items) {
|
|
1047
|
+
const b = el("button", "select-all");
|
|
1048
|
+
b.type = "button";
|
|
1049
|
+
const ids = items.map((i) => i.id);
|
|
1050
|
+
b.__ids = ids;
|
|
1051
|
+
const label = () => {
|
|
1052
|
+
b.textContent = ids.every((id) => ui.selected.has(id)) ? "Clear " + ids.length : "Select all " + ids.length;
|
|
1053
|
+
};
|
|
1054
|
+
b.__label = label;
|
|
1055
|
+
label();
|
|
1056
|
+
b.onclick = () => {
|
|
1057
|
+
const all = ids.every((id) => ui.selected.has(id));
|
|
1058
|
+
for (const id of ids) {
|
|
1059
|
+
if (all) ui.selected.delete(id);
|
|
1060
|
+
else ui.selected.add(id);
|
|
1061
|
+
}
|
|
1062
|
+
for (const id of ids) {
|
|
1063
|
+
const c = document.querySelector('.card[data-key="' + CSS.escape(id) + '"]');
|
|
1064
|
+
if (c) {
|
|
1065
|
+
c.classList.toggle("selected", ui.selected.has(id));
|
|
1066
|
+
c.setAttribute("aria-pressed", String(ui.selected.has(id)));
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
refreshSelectAll();
|
|
1070
|
+
renderSelectionBar();
|
|
1071
|
+
};
|
|
1072
|
+
return b;
|
|
1073
|
+
}
|
|
1074
|
+
function refreshSelectAll() {
|
|
1075
|
+
for (const b of document.querySelectorAll(".select-all")) b.__label?.();
|
|
1076
|
+
}
|
|
1077
|
+
function renderSelectionBar() {
|
|
1078
|
+
const bar = $("selbar");
|
|
1079
|
+
bar.textContent = "";
|
|
1080
|
+
for (const id of [...ui.selected]) if (!S.snap.items.some((i) => i.id === id)) ui.selected.delete(id);
|
|
1081
|
+
bar.hidden = !ui.selecting;
|
|
1082
|
+
if (!ui.selecting) return;
|
|
1083
|
+
const items = selectedItems();
|
|
1084
|
+
const projects = new Set(items.map((i) => i.project));
|
|
1085
|
+
const oneProject = projects.size <= 1;
|
|
1086
|
+
const project = items.length ? items[0].project : null;
|
|
1087
|
+
bar.append(el("b", null, items.length ? items.length + " selected" : "Click cards to select them"));
|
|
1088
|
+
const acts = el("div", "acts");
|
|
1089
|
+
const add = (label, run, opts = {}) => {
|
|
1090
|
+
const b = el("button", opts.primary ? "primary" : null, label);
|
|
1091
|
+
b.type = "button";
|
|
1092
|
+
b.disabled = Boolean(opts.disabled);
|
|
1093
|
+
if (opts.title) b.title = opts.title;
|
|
1094
|
+
b.onclick = run;
|
|
1095
|
+
acts.append(b);
|
|
1096
|
+
return b;
|
|
1097
|
+
};
|
|
1098
|
+
const crossProject = "Select within one project: a job and an edit each belong to one";
|
|
1099
|
+
if (GENERATION) {
|
|
1100
|
+
const todo = items.filter(actionable);
|
|
1101
|
+
if (todo.length) add("Generate " + todo.length + " · " + priceOf(todo), () => generateDialog(todo, { project }), { primary: true, disabled: !oneProject, title: oneProject ? "One job: parents first, then everything drawn from them" : crossProject });
|
|
1102
|
+
const redo = items.filter(regenerable);
|
|
1103
|
+
if (redo.length) add("Regenerate " + redo.length + " · " + priceOf(redo), () => generateDialog(redo, { project, force: true }), { disabled: !oneProject, title: oneProject ? "Replace these with new generations; each old one stays in its history" : crossProject });
|
|
1104
|
+
}
|
|
1105
|
+
const shown = items.filter((i) => i.outputs.some((o) => o.url));
|
|
1106
|
+
if (shown.length >= 2) {
|
|
1107
|
+
add("Compare" + (shown.length > 4 ? " first 4" : ""), () => {
|
|
1108
|
+
ui.compare = shown.slice(0, 4).map((i) => i.id);
|
|
1109
|
+
openCompare();
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (EDITABLE && items.length) {
|
|
1113
|
+
const taggable = items.filter((i) => i.declaredAs && projectOf(i)?.manifestSha256);
|
|
1114
|
+
const tag = el("input");
|
|
1115
|
+
tag.type = "text";
|
|
1116
|
+
tag.placeholder = "tag";
|
|
1117
|
+
tag.setAttribute("aria-label", "Tag to add or remove");
|
|
1118
|
+
tag.pattern = "[^,]+";
|
|
1119
|
+
const addTag = el("button", null, "Add tag");
|
|
1120
|
+
addTag.type = "button";
|
|
1121
|
+
const dropTag = el("button", null, "Remove tag");
|
|
1122
|
+
dropTag.type = "button";
|
|
1123
|
+
addTag.disabled = dropTag.disabled = !taggable.length || !oneProject;
|
|
1124
|
+
addTag.title = dropTag.title = !oneProject ? crossProject : taggable.length < items.length ? "Undeclared records have no manifest entry to tag" : "One manifest write for every selected asset";
|
|
1125
|
+
const run = (adding) => retag(taggable, tag.value.trim(), adding, tag);
|
|
1126
|
+
addTag.onclick = () => run(true);
|
|
1127
|
+
dropTag.onclick = () => run(false);
|
|
1128
|
+
tag.onkeydown = (e) => {
|
|
1129
|
+
if (e.key === "Enter") {
|
|
1130
|
+
e.preventDefault();
|
|
1131
|
+
run(true);
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
const box = el("span", "tagbox");
|
|
1135
|
+
box.append(tag, addTag, dropTag);
|
|
1136
|
+
acts.append(box);
|
|
1137
|
+
}
|
|
1138
|
+
add("Clear", () => {
|
|
1139
|
+
ui.selected.clear();
|
|
1140
|
+
render();
|
|
1141
|
+
}, { disabled: !items.length });
|
|
1142
|
+
add("Done", () => setSelecting(false));
|
|
1143
|
+
bar.append(acts);
|
|
1144
|
+
const msg = el("span", "msg");
|
|
1145
|
+
msg.id = "selmsg";
|
|
1146
|
+
bar.append(msg);
|
|
1147
|
+
}
|
|
1148
|
+
async function retag(items, tag, adding, input) {
|
|
1149
|
+
const msg = $("selmsg");
|
|
1150
|
+
if (!tag || tag.includes(",")) {
|
|
1151
|
+
msg.className = "msg bad";
|
|
1152
|
+
msg.textContent = "Type one tag (no commas).";
|
|
1153
|
+
input.focus();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1157
|
+
for (const i of items) {
|
|
1158
|
+
if (byId.has(i.declaredAs)) continue;
|
|
1159
|
+
const now = i.asset && i.asset.tags || [];
|
|
1160
|
+
const next = adding ? now.includes(tag) ? now : [...now, tag] : now.filter((t) => t !== tag);
|
|
1161
|
+
if (next.length !== now.length) byId.set(i.declaredAs, next);
|
|
1162
|
+
}
|
|
1163
|
+
if (!byId.size) {
|
|
1164
|
+
msg.className = "msg";
|
|
1165
|
+
msg.textContent = adding ? "They all have it already." : "None of them has it.";
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const pr = projectOf(items[0]);
|
|
1169
|
+
const body = {
|
|
1170
|
+
action: "batch",
|
|
1171
|
+
expectedSha256: pr.manifestSha256,
|
|
1172
|
+
edits: [...byId].map(([assetId, tags]) => ({ action: "patch-asset", assetId, patch: { tags } }))
|
|
1173
|
+
};
|
|
1174
|
+
if (items[0].project) body.project = items[0].project;
|
|
1175
|
+
msg.className = "msg";
|
|
1176
|
+
msg.textContent = "saving…";
|
|
1177
|
+
try {
|
|
1178
|
+
S.snap = await postEdit(body);
|
|
1179
|
+
render();
|
|
1180
|
+
const after = $("selmsg");
|
|
1181
|
+
if (after) {
|
|
1182
|
+
after.className = "msg ok";
|
|
1183
|
+
after.textContent = (adding ? "Tagged " : "Untagged ") + byId.size + (byId.size === 1 ? " asset" : " assets") + " “" + tag + "”.";
|
|
1184
|
+
}
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
msg.className = "msg bad";
|
|
1187
|
+
msg.textContent = err.status === 409 ? "The manifest changed on disk; refresh and try again." : err.message;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1015
1191
|
// src/gallery/client/src/grid.ts
|
|
1016
1192
|
function visibleItems() {
|
|
1017
1193
|
const q = ui.q.trim().toLowerCase();
|
|
@@ -1187,7 +1363,9 @@
|
|
|
1187
1363
|
}
|
|
1188
1364
|
function card(item) {
|
|
1189
1365
|
const slot = ui.compare.indexOf(item.id);
|
|
1190
|
-
const
|
|
1366
|
+
const picked = ui.selected.has(item.id);
|
|
1367
|
+
const c = el("button", "card" + (item.outputs.some((o) => o.url) ? "" : " ghost") + (ui.open === item.id ? " active" : "") + (slot >= 0 ? " compared" : "") + (picked ? " selected" : ""));
|
|
1368
|
+
if (ui.selecting) c.setAttribute("aria-pressed", String(picked));
|
|
1191
1369
|
c.type = "button";
|
|
1192
1370
|
c.dataset.key = item.id;
|
|
1193
1371
|
c.setAttribute("aria-label", item.id + ", " + item.state + (slot >= 0 ? ", in comparison" : ""));
|
|
@@ -1216,8 +1394,10 @@
|
|
|
1216
1394
|
cell.append(badge);
|
|
1217
1395
|
}
|
|
1218
1396
|
c.append(cell, body);
|
|
1397
|
+
if (ui.selecting) cell.append(el("span", "sel", "✓"));
|
|
1219
1398
|
c.onclick = (e) => {
|
|
1220
1399
|
if (e.shiftKey) toggleCompare(item.id);
|
|
1400
|
+
else if (ui.selecting || e.metaKey || e.ctrlKey) toggleSelected(item.id);
|
|
1221
1401
|
else openItem(item.id);
|
|
1222
1402
|
};
|
|
1223
1403
|
const still = cell.querySelector(":scope > img");
|
|
@@ -1253,7 +1433,7 @@
|
|
|
1253
1433
|
const sections = [];
|
|
1254
1434
|
for (const pr of projects) {
|
|
1255
1435
|
const inProject = pr ? shown.filter((i) => i.project === pr.id) : shown;
|
|
1256
|
-
const styleSections = ui.group === "style" ? S.snap.styles.filter((s) => !pr || s.project === pr.id).map((s) => ({ style: s, items: inProject.filter((i) => i.styleId === s.id) })).filter((s) => s.items.length) : inProject.length ? [{ style: null, items: inProject }] : [];
|
|
1436
|
+
const styleSections = ui.group === "style" || ui.group === "family" ? S.snap.styles.filter((s) => !pr || s.project === pr.id).map((s) => ({ style: s, items: inProject.filter((i) => i.styleId === s.id) })).filter((s) => s.items.length) : inProject.length ? [{ style: null, items: inProject }] : [];
|
|
1257
1437
|
if (pr) sections.push({ project: pr, style: null, items: null, count: inProject.length });
|
|
1258
1438
|
sections.push(...styleSections);
|
|
1259
1439
|
}
|
|
@@ -1310,6 +1490,7 @@
|
|
|
1310
1490
|
r.onclick = () => generateDialog(refreshable, { project: s.project, refresh: true });
|
|
1311
1491
|
tools.append(r);
|
|
1312
1492
|
}
|
|
1493
|
+
if (ui.selecting && ui.group !== "family") tools.append(selectAllButton(sec.items));
|
|
1313
1494
|
const styleKey = "style:" + (s.project || "") + ":" + s.id;
|
|
1314
1495
|
if (EDITABLE && s.outDir) {
|
|
1315
1496
|
const es = el("button", null, ui.editing === styleKey ? "Cancel" : "Edit style");
|
|
@@ -1334,9 +1515,12 @@
|
|
|
1334
1515
|
if (ui.editing === styleKey) wrap.append(styleForm(s));
|
|
1335
1516
|
if (ui.editing === addKey) wrap.append(addAssetForm(s));
|
|
1336
1517
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1518
|
+
if (ui.group === "family") wrap.append(...familyGroups(sec.items));
|
|
1519
|
+
else {
|
|
1520
|
+
const grid = el("div", "grid");
|
|
1521
|
+
for (const item of sec.items) grid.append(card(item));
|
|
1522
|
+
wrap.append(grid);
|
|
1523
|
+
}
|
|
1340
1524
|
root.append(wrap);
|
|
1341
1525
|
}
|
|
1342
1526
|
if (hidden) {
|
|
@@ -1352,6 +1536,93 @@
|
|
|
1352
1536
|
root.append(more);
|
|
1353
1537
|
}
|
|
1354
1538
|
}
|
|
1539
|
+
var KIND_ORDER = { base: 0, state: 1, animation: 2, portrait: 3, outfit: 4 };
|
|
1540
|
+
var TURN = ["south", "south-east", "east", "north-east", "north", "north-west", "west", "south-west"];
|
|
1541
|
+
function familyOrder(a, b) {
|
|
1542
|
+
const ka = KIND_ORDER[a.character.kind] ?? 9, kb = KIND_ORDER[b.character.kind] ?? 9;
|
|
1543
|
+
if (ka !== kb) return ka - kb;
|
|
1544
|
+
if (a.character.kind === "animation") {
|
|
1545
|
+
const la = loopName(a), lb = loopName(b);
|
|
1546
|
+
if (la !== lb) return la < lb ? -1 : 1;
|
|
1547
|
+
return TURN.indexOf(a.character.direction) - TURN.indexOf(b.character.direction);
|
|
1548
|
+
}
|
|
1549
|
+
return a.assetId < b.assetId ? -1 : a.assetId > b.assetId ? 1 : 0;
|
|
1550
|
+
}
|
|
1551
|
+
function familyGroups(items) {
|
|
1552
|
+
const families = /* @__PURE__ */ new Map();
|
|
1553
|
+
const loose = [];
|
|
1554
|
+
for (const item of items) {
|
|
1555
|
+
const root = item.character ? familyRoot(item) : null;
|
|
1556
|
+
if (!root) {
|
|
1557
|
+
loose.push(item);
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (!families.has(root.id)) families.set(root.id, { root, members: [] });
|
|
1561
|
+
families.get(root.id).members.push(item);
|
|
1562
|
+
}
|
|
1563
|
+
const out = [];
|
|
1564
|
+
for (const { root, members } of families.values()) {
|
|
1565
|
+
members.sort(familyOrder);
|
|
1566
|
+
const group = el("div", "fam-group");
|
|
1567
|
+
const head = el("div", "fam-head");
|
|
1568
|
+
head.append(el("b", "mono", root.assetId));
|
|
1569
|
+
const counts = {};
|
|
1570
|
+
for (const m of members) counts[m.state] = (counts[m.state] || 0) + 1;
|
|
1571
|
+
const tally = el("span", "fam-tally");
|
|
1572
|
+
const loops2 = new Set(members.filter((m) => m.character.kind === "animation").map(loopName)).size;
|
|
1573
|
+
tally.append(document.createTextNode(members.length + (members.length === 1 ? " asset" : " assets") + (loops2 ? " · " + loops2 + (loops2 === 1 ? " loop" : " loops") : "")));
|
|
1574
|
+
for (const [state, n] of Object.entries(counts)) {
|
|
1575
|
+
const t = el("span", "st");
|
|
1576
|
+
t.append(el("i", "dot " + STATE_TONE[state]), document.createTextNode(n + " " + state));
|
|
1577
|
+
tally.append(t);
|
|
1578
|
+
}
|
|
1579
|
+
head.append(tally);
|
|
1580
|
+
const tools = el("div", "tools");
|
|
1581
|
+
if (ui.selecting) tools.append(selectAllButton(members));
|
|
1582
|
+
const view = el("button", null, "Family view");
|
|
1583
|
+
view.type = "button";
|
|
1584
|
+
view.onclick = () => openFamily(root);
|
|
1585
|
+
tools.append(view);
|
|
1586
|
+
const everything = familyMembers(root);
|
|
1587
|
+
const todo = everything.filter(actionable);
|
|
1588
|
+
if (GENERATION && todo.length) {
|
|
1589
|
+
const g = el("button", "primary", "Generate " + todo.length + " · " + priceOf(todo));
|
|
1590
|
+
g.type = "button";
|
|
1591
|
+
g.title = "One job for the family: parents first, then everything drawn from them";
|
|
1592
|
+
g.onclick = () => generateDialog(todo, { project: root.project });
|
|
1593
|
+
tools.append(g);
|
|
1594
|
+
}
|
|
1595
|
+
head.append(tools);
|
|
1596
|
+
const grid = el("div", "grid");
|
|
1597
|
+
for (const m of members) grid.append(card(m));
|
|
1598
|
+
group.append(head, grid);
|
|
1599
|
+
out.push(group);
|
|
1600
|
+
}
|
|
1601
|
+
if (loose.length) {
|
|
1602
|
+
const group = el("div", "fam-group loose");
|
|
1603
|
+
if (families.size) {
|
|
1604
|
+
const head = el("div", "fam-head");
|
|
1605
|
+
head.append(el("b", null, "Not in a family"), el("span", "fam-tally", loose.length + (loose.length === 1 ? " asset" : " assets")));
|
|
1606
|
+
if (ui.selecting) {
|
|
1607
|
+
const tools = el("div", "tools");
|
|
1608
|
+
tools.append(selectAllButton(loose));
|
|
1609
|
+
head.append(tools);
|
|
1610
|
+
}
|
|
1611
|
+
group.append(head);
|
|
1612
|
+
}
|
|
1613
|
+
const grid = el("div", "grid");
|
|
1614
|
+
for (const item of loose) grid.append(card(item));
|
|
1615
|
+
group.append(grid);
|
|
1616
|
+
out.push(group);
|
|
1617
|
+
}
|
|
1618
|
+
return out;
|
|
1619
|
+
}
|
|
1620
|
+
var actionable = (item) => item.declared && item.currentSpecHash !== null && ["missing", "stale", "failed"].includes(item.state);
|
|
1621
|
+
function priceOf(items) {
|
|
1622
|
+
const totals = {};
|
|
1623
|
+
for (const i of items) if (i.estimatedCost !== null) totals[i.costUnit] = (totals[i.costUnit] || 0) + i.estimatedCost;
|
|
1624
|
+
return Object.entries(totals).map(([unit, n]) => fmtCost(unit, Math.round(n * 100) / 100)).join(" + ") || "free";
|
|
1625
|
+
}
|
|
1355
1626
|
function projectHeader(pr, count) {
|
|
1356
1627
|
const head = el("section", "project");
|
|
1357
1628
|
const line = el("div", "phead");
|
|
@@ -3978,7 +4249,7 @@
|
|
|
3978
4249
|
ui.projects = new Set(list("project"));
|
|
3979
4250
|
ui.compare = list("compare").slice(0, 4);
|
|
3980
4251
|
if (["key", "newest", "oldest", "cost", "size"].includes(params.get("sort") ?? "")) ui.sort = params.get("sort");
|
|
3981
|
-
if (["style", "none"].includes(params.get("group") ?? "")) ui.group = params.get("group");
|
|
4252
|
+
if (["style", "family", "none"].includes(params.get("group") ?? "")) ui.group = params.get("group");
|
|
3982
4253
|
}
|
|
3983
4254
|
function writeUrlState() {
|
|
3984
4255
|
const params = new URLSearchParams();
|
|
@@ -4051,6 +4322,7 @@
|
|
|
4051
4322
|
renderMain(visibleItems());
|
|
4052
4323
|
renderDrawer();
|
|
4053
4324
|
renderTray();
|
|
4325
|
+
renderSelectionBar();
|
|
4054
4326
|
refreshFamily();
|
|
4055
4327
|
}
|
|
4056
4328
|
function renderQuietly() {
|
|
@@ -4060,6 +4332,7 @@
|
|
|
4060
4332
|
if (region !== "main") renderMain(visibleItems());
|
|
4061
4333
|
if (region !== "drawer") renderDrawer();
|
|
4062
4334
|
renderTray();
|
|
4335
|
+
renderSelectionBar();
|
|
4063
4336
|
refreshFamily();
|
|
4064
4337
|
}
|
|
4065
4338
|
|
|
@@ -4548,6 +4821,7 @@
|
|
|
4548
4821
|
// src/gallery/client/src/main.ts
|
|
4549
4822
|
loadPrefs();
|
|
4550
4823
|
$("refresh").onclick = refresh;
|
|
4824
|
+
$("select").onclick = () => setSelecting(!ui.selecting);
|
|
4551
4825
|
$("playloops").checked = ui.playLoops;
|
|
4552
4826
|
$("playloops").onchange = (e) => {
|
|
4553
4827
|
setPlayLoops(e.target.checked);
|
|
@@ -4610,6 +4884,11 @@
|
|
|
4610
4884
|
if (ui.open) {
|
|
4611
4885
|
e.preventDefault();
|
|
4612
4886
|
closeItem();
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4889
|
+
if (ui.selecting) {
|
|
4890
|
+
e.preventDefault();
|
|
4891
|
+
setSelecting(false);
|
|
4613
4892
|
}
|
|
4614
4893
|
return;
|
|
4615
4894
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -16660,6 +16660,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16660
16660
|
styleId: spec.styleId,
|
|
16661
16661
|
assetId: spec.assetId,
|
|
16662
16662
|
declared: true,
|
|
16663
|
+
declaredAs: loopShorthandOf(spec.assetId, loaded.loopFamilies) ?? spec.assetId,
|
|
16663
16664
|
state: planItem.state,
|
|
16664
16665
|
reason: planItem.reason,
|
|
16665
16666
|
status: entry?.status ?? null,
|
|
@@ -16729,6 +16730,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16729
16730
|
styleId: entry.styleId,
|
|
16730
16731
|
assetId: entry.assetId,
|
|
16731
16732
|
declared: false,
|
|
16733
|
+
declaredAs: null,
|
|
16732
16734
|
state: "undeclared",
|
|
16733
16735
|
reason: "not declared by the current manifest; `pixelkiln prune` removes the entry",
|
|
16734
16736
|
status: entry.status,
|
|
@@ -17058,9 +17060,11 @@ ${css}</style>
|
|
|
17058
17060
|
</select>
|
|
17059
17061
|
<select id="group" aria-label="Group">
|
|
17060
17062
|
<option value="style">Group: by style</option>
|
|
17063
|
+
<option value="family">Group: by family</option>
|
|
17061
17064
|
<option value="none">Group: none</option>
|
|
17062
17065
|
</select>
|
|
17063
17066
|
<span id="editing" class="editing" hidden title="This gallery can write the manifest. It never contacts a provider.">editing</span>
|
|
17067
|
+
<button id="select" type="button" title="Pick several records to generate, regenerate, compare, or tag together (or ctrl/\u2318-click a card)">Select</button>
|
|
17064
17068
|
<button id="studio" type="button" hidden title="Draft a new character: its style, base, loops, and portrait, priced as you go">+ New character</button>
|
|
17065
17069
|
<button id="refresh" type="button" title="Re-read the manifest, lockfile, and disk">Refresh</button>
|
|
17066
17070
|
<label class="chip" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> auto</label>
|
|
@@ -17074,6 +17078,7 @@ ${css}</style>
|
|
|
17074
17078
|
<div id="tools"></div>
|
|
17075
17079
|
</header>
|
|
17076
17080
|
<main id="root"></main>
|
|
17081
|
+
<div id="selbar" class="selbar" hidden></div>
|
|
17077
17082
|
<footer>
|
|
17078
17083
|
Click a sprite for its full record; <kbd>shift</kbd>-click adds it to a side-by-side comparison. <kbd>\u2190</kbd>/<kbd>\u2192</kbd> step through the visible set while a record
|
|
17079
17084
|
is open, <kbd>,</kbd>/<kbd>.</kbd> step a loop's frames and <kbd>space</kbd> plays it, <kbd>Esc</kbd> closes it, and <kbd>/</kbd> jumps to search. This page reads the manifest, lockfile,
|