@iasiv5/dsh-skins 1.5.11 → 1.5.13
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/lib/client.js +170 -52
- package/lib/index.js +95 -26
- package/package.json +4 -2
package/lib/client.js
CHANGED
|
@@ -940,9 +940,11 @@ ${darkSelector}{${dark.join(";")}}`;
|
|
|
940
940
|
"personalization.panelLabel": "个性化设置",
|
|
941
941
|
"personalization.saveFailed": "保存失败,请重试",
|
|
942
942
|
"personalization.saveConflict": "配置已在其他窗口更新,请重试",
|
|
943
|
+
"personalization.fieldReverted": "壁纸已失效,已回退默认",
|
|
944
|
+
"personalization.library.previewRef": "未保存引用",
|
|
943
945
|
"personalization.library.clear": "清空图库",
|
|
944
946
|
"personalization.library.clearConfirm": "将永久删除全部 {count} 张上传图片,且不可恢复。以下壁纸设置将回退为默认:{names}",
|
|
945
|
-
"personalization.library.clearFailed": "
|
|
947
|
+
"personalization.library.clearFailed": "清空图库失败,已刷新剩余列表",
|
|
946
948
|
"personalization.library.uploading": "正在上传…",
|
|
947
949
|
"personalization.library.uploadingBatch": "正在上传 {done}/{total}…",
|
|
948
950
|
"personalization.library.uploadSomeFailed": "已上传 {ok} 张,{failed} 张失败({reason})",
|
|
@@ -1013,9 +1015,11 @@ ${darkSelector}{${dark.join(";")}}`;
|
|
|
1013
1015
|
"personalization.panelLabel": "Personalization settings",
|
|
1014
1016
|
"personalization.saveFailed": "Save failed; try again",
|
|
1015
1017
|
"personalization.saveConflict": "Settings changed in another window; try again",
|
|
1018
|
+
"personalization.fieldReverted": "Wallpaper no longer exists; reset to default",
|
|
1019
|
+
"personalization.library.previewRef": "unsaved reference",
|
|
1016
1020
|
"personalization.library.clear": "Clear library",
|
|
1017
1021
|
"personalization.library.clearConfirm": "This permanently deletes all {count} uploaded images. These wallpaper settings fall back to defaults: {names}",
|
|
1018
|
-
"personalization.library.clearFailed": "
|
|
1022
|
+
"personalization.library.clearFailed": "Clear library failed; the remaining list was refreshed",
|
|
1019
1023
|
"personalization.library.uploading": "Uploading…",
|
|
1020
1024
|
"personalization.library.uploadingBatch": "Uploading {done}/{total}…",
|
|
1021
1025
|
"personalization.library.uploadSomeFailed": "Uploaded {ok}; {failed} failed ({reason})",
|
|
@@ -1260,39 +1264,36 @@ ${darkSelector}{${dark.join(";")}}`;
|
|
|
1260
1264
|
setDeletingId(null);
|
|
1261
1265
|
}
|
|
1262
1266
|
};
|
|
1267
|
+
const referenceLabelsFor = (assetId) => configClient.referencesFor(assetId).map((entry) => entry.layer === "preview" ? `${labelFor(entry.skinId)} · ${entry.key} · ${tr("personalization.library.previewRef")}` : `${labelFor(entry.skinId)} · ${entry.key}`);
|
|
1263
1268
|
const removeAsset = (asset) => {
|
|
1264
1269
|
if (deletingId !== null || clearing) return;
|
|
1265
|
-
const references =
|
|
1270
|
+
const references = referenceLabelsFor(asset.id);
|
|
1266
1271
|
if (references.length === 0) return deleteAsset(asset);
|
|
1267
|
-
const names = references.map((entry) => `${labelFor(entry.skinId)} · ${entry.key}`).join("\n");
|
|
1268
1272
|
requestConfirm(
|
|
1269
1273
|
`${tr("personalization.library.deleteConfirm")}
|
|
1270
|
-
${
|
|
1274
|
+
${references.join("\n")}`,
|
|
1271
1275
|
() => deleteAsset(asset)
|
|
1272
1276
|
);
|
|
1273
1277
|
};
|
|
1274
1278
|
const clearLibrary = () => {
|
|
1275
1279
|
if (clearing || deletingId !== null || library.length === 0) return;
|
|
1276
|
-
const
|
|
1280
|
+
const names = /* @__PURE__ */ new Map();
|
|
1277
1281
|
for (const asset of library) {
|
|
1278
|
-
for (const
|
|
1279
|
-
|
|
1282
|
+
for (const label of referenceLabelsFor(asset.id)) {
|
|
1283
|
+
names.set(label, true);
|
|
1280
1284
|
}
|
|
1281
1285
|
}
|
|
1282
|
-
const
|
|
1286
|
+
const listed = [...names.keys()].join("、");
|
|
1283
1287
|
requestConfirm(tr("personalization.library.clearConfirm", {
|
|
1284
1288
|
count: library.length,
|
|
1285
|
-
names:
|
|
1289
|
+
names: listed === "" ? "—" : listed
|
|
1286
1290
|
}), async () => {
|
|
1287
1291
|
setClearing(true);
|
|
1288
1292
|
try {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
void configClient.refetch();
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1293
|
+
const result = await configClient.clearLibrary();
|
|
1294
|
+
if (result.error) {
|
|
1295
|
+
setMessage(tr("personalization.library.clearFailed"));
|
|
1296
|
+
void configClient.refetch();
|
|
1296
1297
|
}
|
|
1297
1298
|
} catch {
|
|
1298
1299
|
setMessage(tr("personalization.library.clearFailed"));
|
|
@@ -1405,7 +1406,10 @@ ${names}`,
|
|
|
1405
1406
|
);
|
|
1406
1407
|
}
|
|
1407
1408
|
}
|
|
1408
|
-
if (lastAssetId !== null)
|
|
1409
|
+
if (lastAssetId !== null) {
|
|
1410
|
+
onValue(lastAssetId);
|
|
1411
|
+
await configClient.flushNow();
|
|
1412
|
+
}
|
|
1409
1413
|
setMessage(firstError === null ? null : ok === 0 ? firstError : tr("personalization.library.uploadSomeFailed", {
|
|
1410
1414
|
ok,
|
|
1411
1415
|
failed: files.length - ok,
|
|
@@ -1417,6 +1421,7 @@ ${names}`,
|
|
|
1417
1421
|
if (result.asset) {
|
|
1418
1422
|
setMessage(null);
|
|
1419
1423
|
onValue(result.asset.id);
|
|
1424
|
+
await configClient.flushNow();
|
|
1420
1425
|
} else {
|
|
1421
1426
|
setMessage(resolveHostErrorText(
|
|
1422
1427
|
{ code: result.error, message: tr("personalization.library.uploadFailed") },
|
|
@@ -1470,8 +1475,7 @@ ${names}`,
|
|
|
1470
1475
|
if (confirmationRef.current !== confirmation) return;
|
|
1471
1476
|
confirmationRef.current = null;
|
|
1472
1477
|
setConfirmation(null);
|
|
1473
|
-
|
|
1474
|
-
if (current.status === "synced" && current.mode !== "recovery") {
|
|
1478
|
+
if (configClient.canWrite()) {
|
|
1475
1479
|
void confirmation.accept();
|
|
1476
1480
|
}
|
|
1477
1481
|
};
|
|
@@ -1483,7 +1487,7 @@ ${names}`,
|
|
|
1483
1487
|
headerRef.current?.focus?.(wideLayout ? { preventScroll: true } : void 0);
|
|
1484
1488
|
}, []);
|
|
1485
1489
|
if (schema === null) return null;
|
|
1486
|
-
const writesBlocked =
|
|
1490
|
+
const writesBlocked = !configClient.canWrite();
|
|
1487
1491
|
const overrides = configClient.effectiveOverrides(skinId);
|
|
1488
1492
|
const { values } = mergeValues(skinId, overrides, void 0, profile);
|
|
1489
1493
|
const hasAnyOverride = Object.keys(overrides).length > 0;
|
|
@@ -1534,7 +1538,7 @@ ${names}`,
|
|
|
1534
1538
|
jsx("button", {
|
|
1535
1539
|
type: "button",
|
|
1536
1540
|
className: "dsh-skins-pz-btn",
|
|
1537
|
-
onClick: () => configClient.
|
|
1541
|
+
onClick: () => configClient.refetch(),
|
|
1538
1542
|
children: tr("personalization.status.retry")
|
|
1539
1543
|
})
|
|
1540
1544
|
] }));
|
|
@@ -1544,14 +1548,27 @@ ${names}`,
|
|
|
1544
1548
|
jsx("span", { children: tr("personalization.status.unsupported") })
|
|
1545
1549
|
] }));
|
|
1546
1550
|
}
|
|
1547
|
-
|
|
1551
|
+
const notices = state.notices;
|
|
1552
|
+
const conflictNotice = notices.find((notice) => notice.kind === "conflict");
|
|
1553
|
+
const errorNotice = notices.find((notice) => notice.kind === "error");
|
|
1554
|
+
if (conflictNotice !== void 0) {
|
|
1548
1555
|
statusCluster.push(jsx("div", { key: "save-conflict", className: "dsh-skins-pz-status dsh-skins-pz-warn", children: [
|
|
1549
1556
|
jsx("span", { children: tr("personalization.saveConflict") })
|
|
1550
1557
|
] }));
|
|
1551
|
-
} else if (
|
|
1558
|
+
} else if (errorNotice !== void 0) {
|
|
1552
1559
|
statusCluster.push(jsx("div", { key: "save-error", className: "dsh-skins-pz-status dsh-skins-pz-warn", children: [
|
|
1553
1560
|
jsx("span", { children: tr("personalization.saveFailed") }),
|
|
1554
|
-
jsx("div", { className: "dsh-skins-pz-muted", children:
|
|
1561
|
+
jsx("div", { className: "dsh-skins-pz-muted", children: errorNotice.message })
|
|
1562
|
+
] }));
|
|
1563
|
+
}
|
|
1564
|
+
const revertedNotice = notices.find((notice) => notice.kind === "reverted");
|
|
1565
|
+
if (revertedNotice !== void 0 && revertedNotice.fields.length > 0) {
|
|
1566
|
+
statusCluster.push(jsx("div", { key: "reverted", className: "dsh-skins-pz-status", children: [
|
|
1567
|
+
jsx("div", { children: tr("personalization.fieldReverted") }),
|
|
1568
|
+
jsx("div", {
|
|
1569
|
+
className: "dsh-skins-pz-muted",
|
|
1570
|
+
children: revertedNotice.fields.map((entry) => `${labelFor(entry.skinId)} · ${entry.key}`).join("、")
|
|
1571
|
+
})
|
|
1555
1572
|
] }));
|
|
1556
1573
|
}
|
|
1557
1574
|
return jsx("div", { className: "dsh-skins-pz", children: [
|
|
@@ -2191,12 +2208,20 @@ ${names}`,
|
|
|
2191
2208
|
let fetchGeneration = 0;
|
|
2192
2209
|
let disposed = false;
|
|
2193
2210
|
let writeChain = Promise.resolve();
|
|
2194
|
-
let
|
|
2195
|
-
let lastFlushError = null;
|
|
2211
|
+
let notices = [];
|
|
2196
2212
|
let channel = null;
|
|
2197
2213
|
function setFlushFailure(code, message) {
|
|
2198
|
-
|
|
2199
|
-
|
|
2214
|
+
notices = notices.filter((notice) => notice.kind === "reverted");
|
|
2215
|
+
if (code === "REVISION_CONFLICT") notices.push({ kind: "conflict" });
|
|
2216
|
+
else if (message) notices.push({ kind: "error", message });
|
|
2217
|
+
}
|
|
2218
|
+
function noteReverted(entry) {
|
|
2219
|
+
const current = notices.find((notice) => notice.kind === "reverted");
|
|
2220
|
+
if (current !== void 0 && current.fields.some((field) => field.skinId === entry.skinId && field.key === entry.key)) return;
|
|
2221
|
+
notices = [
|
|
2222
|
+
...notices.filter((notice) => notice.kind !== "reverted"),
|
|
2223
|
+
{ kind: "reverted", fields: [...current?.fields ?? [], entry] }
|
|
2224
|
+
];
|
|
2200
2225
|
}
|
|
2201
2226
|
function emit() {
|
|
2202
2227
|
for (const listener of [...listeners]) {
|
|
@@ -2216,8 +2241,7 @@ ${names}`,
|
|
|
2216
2241
|
references: snapshot.references ?? {},
|
|
2217
2242
|
recovery: snapshot.recovery,
|
|
2218
2243
|
quota: snapshot.quota,
|
|
2219
|
-
|
|
2220
|
-
lastFlushError,
|
|
2244
|
+
notices: structuredClone(notices),
|
|
2221
2245
|
dirtyCount: previews.size
|
|
2222
2246
|
};
|
|
2223
2247
|
}
|
|
@@ -2281,8 +2305,10 @@ ${names}`,
|
|
|
2281
2305
|
}
|
|
2282
2306
|
const FLUSH_DEBOUNCE_MS = 400;
|
|
2283
2307
|
let flushTimer = null;
|
|
2284
|
-
function
|
|
2285
|
-
if (status === "synced")
|
|
2308
|
+
function writeGate() {
|
|
2309
|
+
if (status === "synced") {
|
|
2310
|
+
return snapshot.mode === "recovery" ? "recovery" : null;
|
|
2311
|
+
}
|
|
2286
2312
|
if (status === "loading") return "loading";
|
|
2287
2313
|
if (status === "unsupported-readonly") return "unsupported";
|
|
2288
2314
|
return "offline";
|
|
@@ -2309,7 +2335,7 @@ ${names}`,
|
|
|
2309
2335
|
writeChain = writeChain.then(async () => {
|
|
2310
2336
|
try {
|
|
2311
2337
|
if (disposed) return { flushed: 0, blocked: "disposed" };
|
|
2312
|
-
const blocked =
|
|
2338
|
+
const blocked = writeGate();
|
|
2313
2339
|
if (blocked !== null) return { flushed: 0, blocked };
|
|
2314
2340
|
const attemptedRevision = snapshot.revision;
|
|
2315
2341
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -2374,8 +2400,28 @@ ${names}`,
|
|
|
2374
2400
|
if (!response.ok) {
|
|
2375
2401
|
const errorBody = await response.json().catch(() => null);
|
|
2376
2402
|
const errorMessage = errorBody?.error ?? `HTTP ${response.status}`;
|
|
2377
|
-
|
|
2403
|
+
const issues = Array.isArray(errorBody?.params?.issues) ? errorBody.params.issues : null;
|
|
2404
|
+
let excised = false;
|
|
2405
|
+
let onlyDangling = issues !== null && issues.length > 0;
|
|
2406
|
+
if (issues !== null) {
|
|
2407
|
+
for (const issue of issues) {
|
|
2408
|
+
const dangling = issue.code === "MISSING_ASSET" || issue.code === "BAD_ASSET";
|
|
2409
|
+
if (!dangling) onlyDangling = false;
|
|
2410
|
+
if (dangling && typeof issue.skinId === "string" && typeof issue.key === "string") {
|
|
2411
|
+
const composite = `${issue.skinId} ${issue.key}`;
|
|
2412
|
+
if (previews.has(composite)) {
|
|
2413
|
+
previews.delete(composite);
|
|
2414
|
+
excised = true;
|
|
2415
|
+
noteReverted({ skinId: issue.skinId, key: issue.key });
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
const healed = onlyDangling && excised;
|
|
2421
|
+
setFlushFailure(null, healed ? null : errorMessage);
|
|
2378
2422
|
emit();
|
|
2423
|
+
if (excised && previews.size > 0) void flushNow();
|
|
2424
|
+
if (healed) return { flushed: 0, blocked: null, selfHealed: true };
|
|
2379
2425
|
return { flushed: 0, blocked: "error", errorMessage };
|
|
2380
2426
|
}
|
|
2381
2427
|
await response.json().catch(() => null);
|
|
@@ -2383,6 +2429,7 @@ ${names}`,
|
|
|
2383
2429
|
if (previews.get(composite) === value) previews.delete(composite);
|
|
2384
2430
|
}
|
|
2385
2431
|
setFlushFailure(null, null);
|
|
2432
|
+
notices = [];
|
|
2386
2433
|
await refetch();
|
|
2387
2434
|
announce();
|
|
2388
2435
|
return { flushed: count };
|
|
@@ -2440,10 +2487,6 @@ ${names}`,
|
|
|
2440
2487
|
}
|
|
2441
2488
|
return merged;
|
|
2442
2489
|
}
|
|
2443
|
-
function retry() {
|
|
2444
|
-
if (status !== "offline-failed" && status !== "synced") return refetch();
|
|
2445
|
-
return refetch();
|
|
2446
|
-
}
|
|
2447
2490
|
async function assetFingerprint(bytes) {
|
|
2448
2491
|
if (typeof crypto?.subtle?.digest !== "function") return null;
|
|
2449
2492
|
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
@@ -2477,7 +2520,7 @@ ${names}`,
|
|
|
2477
2520
|
return { error: `HTTP ${response.status}` };
|
|
2478
2521
|
}
|
|
2479
2522
|
async function uploadImage(file, displayName) {
|
|
2480
|
-
const blocked =
|
|
2523
|
+
const blocked = writeGate();
|
|
2481
2524
|
if (blocked !== null) return { error: blocked };
|
|
2482
2525
|
const bytes = file instanceof Uint8Array ? file : new Uint8Array(await file.arrayBuffer());
|
|
2483
2526
|
let networkError;
|
|
@@ -2508,22 +2551,96 @@ ${names}`,
|
|
|
2508
2551
|
async function concludeUpload(outcome) {
|
|
2509
2552
|
if (outcome.error !== void 0) return { error: outcome.error };
|
|
2510
2553
|
const body = await outcome.response.json();
|
|
2511
|
-
|
|
2554
|
+
if (body?.asset?.id !== void 0) {
|
|
2555
|
+
snapshot = {
|
|
2556
|
+
...snapshot,
|
|
2557
|
+
revision: typeof body.revision === "number" ? body.revision : snapshot.revision,
|
|
2558
|
+
library: [...snapshot.library, body.asset],
|
|
2559
|
+
quota: {
|
|
2560
|
+
count: (snapshot.quota?.count ?? 0) + 1,
|
|
2561
|
+
totalBytes: (snapshot.quota?.totalBytes ?? 0) + (body.asset.byteLength ?? 0)
|
|
2562
|
+
}
|
|
2563
|
+
};
|
|
2564
|
+
emit();
|
|
2565
|
+
}
|
|
2512
2566
|
announce();
|
|
2513
2567
|
return { asset: body.asset };
|
|
2514
2568
|
}
|
|
2569
|
+
function runGalleryOp(operations, goalAchieved) {
|
|
2570
|
+
writeChain = writeChain.then(async () => {
|
|
2571
|
+
if (disposed) return { error: "disposed" };
|
|
2572
|
+
const blocked = writeGate();
|
|
2573
|
+
if (blocked !== null) return { error: blocked };
|
|
2574
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2575
|
+
let response;
|
|
2576
|
+
try {
|
|
2577
|
+
response = await request("/config", {
|
|
2578
|
+
method: "PATCH",
|
|
2579
|
+
headers: { "content-type": "application/json" },
|
|
2580
|
+
body: JSON.stringify({ baseRevision: snapshot.revision, operations })
|
|
2581
|
+
});
|
|
2582
|
+
} catch {
|
|
2583
|
+
return { error: "OFFLINE" };
|
|
2584
|
+
}
|
|
2585
|
+
if (disposed) return { error: "disposed" };
|
|
2586
|
+
if (response.status === 409) {
|
|
2587
|
+
const conflictBody = await response.json().catch(() => null);
|
|
2588
|
+
if (conflictBody?.code === "STORE_READONLY") {
|
|
2589
|
+
setFlushFailure(null, null);
|
|
2590
|
+
setStatus("unsupported-readonly");
|
|
2591
|
+
return { error: "conflict" };
|
|
2592
|
+
}
|
|
2593
|
+
await refetch();
|
|
2594
|
+
if (goalAchieved()) return { affectedSkins: [] };
|
|
2595
|
+
if (attempt === 1) {
|
|
2596
|
+
setFlushFailure(null, null);
|
|
2597
|
+
emit();
|
|
2598
|
+
return { error: "REVISION_CONFLICT" };
|
|
2599
|
+
}
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
if (!response.ok) {
|
|
2603
|
+
const body2 = await response.json().catch(() => null);
|
|
2604
|
+
if (body2?.code === "INVALID_CONFIG" && body2.params?.issues?.some((issue) => issue.code === "ASSET_NOT_FOUND")) {
|
|
2605
|
+
await refetch();
|
|
2606
|
+
if (goalAchieved()) return { affectedSkins: [] };
|
|
2607
|
+
return { error: "ASSET_NOT_FOUND" };
|
|
2608
|
+
}
|
|
2609
|
+
return { error: body2?.code ?? `HTTP ${response.status}` };
|
|
2610
|
+
}
|
|
2611
|
+
const body = await response.json().catch(() => null);
|
|
2612
|
+
await refetch();
|
|
2613
|
+
announce();
|
|
2614
|
+
return { affectedSkins: body?.affectedSkins ?? [], revision: body?.revision };
|
|
2615
|
+
}
|
|
2616
|
+
return { error: "REVISION_CONFLICT" };
|
|
2617
|
+
});
|
|
2618
|
+
return writeChain;
|
|
2619
|
+
}
|
|
2515
2620
|
async function deleteImage(id) {
|
|
2516
|
-
const blocked =
|
|
2621
|
+
const blocked = writeGate();
|
|
2517
2622
|
if (blocked !== null) return { error: blocked };
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2623
|
+
return runGalleryOp(
|
|
2624
|
+
[{ op: "deleteAsset", assetId: id }],
|
|
2625
|
+
() => !snapshot.library.some((entry) => entry.id === id)
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
function clearLibrary() {
|
|
2629
|
+
return runGalleryOp([{ op: "clearLibrary" }], () => snapshot.library.length === 0);
|
|
2630
|
+
}
|
|
2631
|
+
function valuedPreviews() {
|
|
2632
|
+
const fields = [];
|
|
2633
|
+
for (const [composite, value] of previews) {
|
|
2634
|
+
if (value === null) continue;
|
|
2635
|
+
const [skinId, key] = composite.split(" ");
|
|
2636
|
+
fields.push({ skinId, key, value });
|
|
2522
2637
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2638
|
+
return fields;
|
|
2639
|
+
}
|
|
2640
|
+
function referencesFor(assetId) {
|
|
2641
|
+
const persisted = (snapshot.references?.[assetId] ?? []).map((entry) => ({ skinId: entry.skinId, key: entry.key, layer: "persisted" }));
|
|
2642
|
+
const preview2 = valuedPreviews().filter((entry) => entry.value === assetId).map((entry) => ({ skinId: entry.skinId, key: entry.key, layer: "preview" }));
|
|
2643
|
+
return [...persisted, ...preview2];
|
|
2527
2644
|
}
|
|
2528
2645
|
async function confirmRecovery() {
|
|
2529
2646
|
const response = await request("/recovery", { method: "POST" });
|
|
@@ -2541,17 +2658,18 @@ ${names}`,
|
|
|
2541
2658
|
const api = {
|
|
2542
2659
|
boot: refetch,
|
|
2543
2660
|
refetch,
|
|
2544
|
-
retry,
|
|
2545
2661
|
flushNow,
|
|
2546
2662
|
preview,
|
|
2547
2663
|
previewReset,
|
|
2548
2664
|
effectiveOverrides,
|
|
2549
2665
|
uploadImage,
|
|
2550
2666
|
deleteImage,
|
|
2667
|
+
clearLibrary,
|
|
2668
|
+
referencesFor,
|
|
2551
2669
|
confirmRecovery,
|
|
2552
2670
|
assetUrl,
|
|
2553
2671
|
getState: publicState,
|
|
2554
|
-
|
|
2672
|
+
canWrite: () => writeGate() === null,
|
|
2555
2673
|
onStateChange(listener) {
|
|
2556
2674
|
listeners.add(listener);
|
|
2557
2675
|
return () => listeners.delete(listener);
|
package/lib/index.js
CHANGED
|
@@ -693,6 +693,20 @@ function createPersonalizationStore(options = {}) {
|
|
|
693
693
|
if (mode === "recovery") base.recovery = recovery;
|
|
694
694
|
return base;
|
|
695
695
|
}
|
|
696
|
+
function applyGalleryDelete(draft, id, affectedSkins) {
|
|
697
|
+
const meta = draft.library[id];
|
|
698
|
+
for (const [skinId, section] of Object.entries(draft.skins)) {
|
|
699
|
+
for (const field of listAssetFields(skinId)) {
|
|
700
|
+
if (section?.[field.key] === id) {
|
|
701
|
+
delete section[field.key];
|
|
702
|
+
affectedSkins.push({ skinId, key: field.key });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (Object.keys(section).length === 0) delete draft.skins[skinId];
|
|
706
|
+
}
|
|
707
|
+
delete draft.library[id];
|
|
708
|
+
return meta;
|
|
709
|
+
}
|
|
696
710
|
async function applyOperations({ baseRevision, operations }) {
|
|
697
711
|
init();
|
|
698
712
|
requireNormal("配置写入");
|
|
@@ -703,36 +717,100 @@ function createPersonalizationStore(options = {}) {
|
|
|
703
717
|
if (!Array.isArray(operations) || operations.length === 0 || operations.length > 64) {
|
|
704
718
|
throw codedError("INVALID_CONFIG", "operations 必须是 1–64 个元素的数组");
|
|
705
719
|
}
|
|
706
|
-
|
|
720
|
+
const issues = [];
|
|
721
|
+
for (let index = 0; index < operations.length; index += 1) {
|
|
722
|
+
const operation = operations[index];
|
|
707
723
|
if (operation === null || typeof operation !== "object") {
|
|
708
|
-
|
|
724
|
+
issues.push({ opIndex: index, code: "INVALID_OP" });
|
|
725
|
+
continue;
|
|
709
726
|
}
|
|
710
727
|
const { op, skinId, key } = operation;
|
|
711
|
-
if (op
|
|
712
|
-
|
|
713
|
-
|
|
728
|
+
if (op === "set" || op === "delete") {
|
|
729
|
+
if (getField(skinId, key) === null) {
|
|
730
|
+
issues.push({ opIndex: index, skinId, key, code: "UNKNOWN_FIELD" });
|
|
731
|
+
}
|
|
732
|
+
} else if (op === "deleteAsset") {
|
|
733
|
+
const assetId = operation.assetId;
|
|
734
|
+
if (typeof assetId !== "string" || !ASSET_ID_PATTERN.test(assetId)) {
|
|
735
|
+
issues.push({ opIndex: index, assetId, code: "INVALID_ASSET_ID" });
|
|
736
|
+
} else if (state.library[assetId] === void 0) {
|
|
737
|
+
issues.push({ opIndex: index, assetId, code: "ASSET_NOT_FOUND" });
|
|
738
|
+
}
|
|
739
|
+
} else if (op !== "clearLibrary") {
|
|
740
|
+
issues.push({ opIndex: index, code: "INVALID_OP" });
|
|
714
741
|
}
|
|
715
742
|
}
|
|
716
743
|
const draft = structuredClone(state);
|
|
717
744
|
const provider = metaProviderFactory(draft.library);
|
|
745
|
+
for (let index = 0; index < operations.length; index += 1) {
|
|
746
|
+
const operation = operations[index];
|
|
747
|
+
if (operation?.op !== "set") continue;
|
|
748
|
+
const verdict = validateOverride(operation.skinId, operation.key, operation.value, provider);
|
|
749
|
+
if (!verdict.ok) {
|
|
750
|
+
issues.push({ opIndex: index, skinId: operation.skinId, key: operation.key, code: verdict.code });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (issues.length > 0) {
|
|
754
|
+
issues.sort((a, b) => a.opIndex - b.opIndex);
|
|
755
|
+
throw codedError("INVALID_CONFIG", `operations 校验失败(${issues[0].code})`, {
|
|
756
|
+
code: issues[0].code,
|
|
757
|
+
// retained for old readers (ADR-0015 Q&A R1-8)
|
|
758
|
+
issues
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
const affectedSkins = [];
|
|
762
|
+
const removedAssets = [];
|
|
763
|
+
let galleryOps = false;
|
|
764
|
+
const hasFieldOps = operations.some((operation) => operation?.op === "set" || operation?.op === "delete");
|
|
718
765
|
for (const operation of operations) {
|
|
719
766
|
const { op, skinId, key } = operation;
|
|
720
|
-
if (op === "set") {
|
|
721
|
-
|
|
722
|
-
if (
|
|
723
|
-
|
|
767
|
+
if (op === "set" || op === "delete") {
|
|
768
|
+
draft.skins[skinId] = draft.skins[skinId] ?? {};
|
|
769
|
+
if (op === "set") draft.skins[skinId][key] = operation.value;
|
|
770
|
+
else delete draft.skins[skinId][key];
|
|
771
|
+
} else if (op === "deleteAsset") {
|
|
772
|
+
galleryOps = true;
|
|
773
|
+
const meta = applyGalleryDelete(draft, operation.assetId, affectedSkins);
|
|
774
|
+
if (meta !== void 0) removedAssets.push(meta);
|
|
775
|
+
} else if (op === "clearLibrary") {
|
|
776
|
+
galleryOps = true;
|
|
777
|
+
for (const id of Object.keys(draft.library)) {
|
|
778
|
+
const meta = applyGalleryDelete(draft, id, affectedSkins);
|
|
779
|
+
if (meta !== void 0) removedAssets.push(meta);
|
|
724
780
|
}
|
|
725
781
|
}
|
|
726
782
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
783
|
+
if (galleryOps && !hasFieldOps && removedAssets.length === 0) {
|
|
784
|
+
return { revision: state.revision, affectedSkins };
|
|
785
|
+
}
|
|
786
|
+
if (galleryOps) {
|
|
787
|
+
const finalProvider = metaProviderFactory(draft.library);
|
|
788
|
+
for (const [skinId, section] of Object.entries(draft.skins)) {
|
|
789
|
+
for (const field of listAssetFields(skinId)) {
|
|
790
|
+
const value = section?.[field.key];
|
|
791
|
+
if (value === void 0) continue;
|
|
792
|
+
const verdict = validateOverride(skinId, field.key, value, finalProvider);
|
|
793
|
+
if (!verdict.ok) {
|
|
794
|
+
throw codedError("INVALID_CONFIG", `终态校验失败:${skinId}.${field.key} 引用已删除资产`, {
|
|
795
|
+
code: verdict.code,
|
|
796
|
+
issues: [{ skinId, key: field.key, code: verdict.code }]
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
731
801
|
}
|
|
732
802
|
draft.revision += 1;
|
|
733
803
|
commitState(draft);
|
|
804
|
+
for (const meta of removedAssets) {
|
|
805
|
+
try {
|
|
806
|
+
fs.unlinkSync(blobFileFor(meta.id, meta.extension));
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
}
|
|
734
810
|
gcNow();
|
|
735
|
-
|
|
811
|
+
const result = { revision: draft.revision };
|
|
812
|
+
if (galleryOps) result.affectedSkins = affectedSkins;
|
|
813
|
+
return result;
|
|
736
814
|
});
|
|
737
815
|
}
|
|
738
816
|
function ingestAssetMeta(buffer, { declaredMime } = {}) {
|
|
@@ -829,23 +907,14 @@ function createPersonalizationStore(options = {}) {
|
|
|
829
907
|
throw codedError("INVALID_ASSET_ID", "非法的图片 id");
|
|
830
908
|
}
|
|
831
909
|
return enqueue(() => {
|
|
832
|
-
|
|
833
|
-
if (meta === void 0) throw codedError("ASSET_NOT_FOUND", "图片不存在");
|
|
910
|
+
if (state.library[id] === void 0) throw codedError("ASSET_NOT_FOUND", "图片不存在");
|
|
834
911
|
const draft = structuredClone(state);
|
|
835
912
|
const affectedSkins = [];
|
|
836
|
-
|
|
837
|
-
for (const field of listAssetFields(skinId)) {
|
|
838
|
-
if (section?.[field.key] === id) {
|
|
839
|
-
delete section[field.key];
|
|
840
|
-
affectedSkins.push({ skinId, key: field.key });
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
delete draft.library[id];
|
|
913
|
+
const meta = applyGalleryDelete(draft, id, affectedSkins);
|
|
845
914
|
draft.revision += 1;
|
|
846
915
|
commitState(draft);
|
|
847
916
|
try {
|
|
848
|
-
fs.unlinkSync(blobFileFor(id, meta.extension));
|
|
917
|
+
fs.unlinkSync(blobFileFor(meta.id, meta.extension));
|
|
849
918
|
} catch {
|
|
850
919
|
}
|
|
851
920
|
gcNow();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iasiv5/dsh-skins",
|
|
3
3
|
"description": "DSH Web 皮肤插件:多套品牌皮肤侧栏热切换。",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.13",
|
|
5
5
|
"homepage": "https://github.com/iasiv5/dsh-skins#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
68
68
|
"esbuild": "^0.28.2",
|
|
69
|
+
"eslint": "^10.13.0",
|
|
69
70
|
"playwright-core": "1.62.1",
|
|
70
71
|
"semver": "^7.8.5"
|
|
71
72
|
},
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"verify:readme": "node scripts/verify-readme-pairing.mjs",
|
|
78
79
|
"verify:bundle": "node scripts/verify-bundle-guard.mjs",
|
|
79
80
|
"verify:hooks": "node scripts/verify-upstream-hooks.mjs",
|
|
80
|
-
"check": "pnpm run check:lockfile && pnpm run build && node --check lib/client.js && node --check lib/index.js && pnpm run test && pnpm run verify:readme && pnpm run verify:bundle && pnpm run verify:hooks"
|
|
81
|
+
"check": "pnpm run check:lockfile && pnpm run build && node --check lib/client.js && node --check lib/index.js && pnpm run lint:tests && pnpm run test && pnpm run verify:readme && pnpm run verify:bundle && pnpm run verify:hooks && git diff --exit-code -- lib/index.js lib/client.js",
|
|
82
|
+
"lint:tests": "eslint tests/"
|
|
81
83
|
}
|
|
82
84
|
}
|