dsh-background-by-model 0.10.0 → 0.12.0-test.1
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/CHANGELOG.md +66 -1
- package/CHANGELOG.zh.md +66 -1
- package/README.md +49 -16
- package/README.zh.md +47 -14
- package/cordis.patch.yml +2 -2
- package/lib/client.js +552 -150
- package/lib/index.js +8163 -297
- package/package.json +7 -3
package/lib/client.js
CHANGED
|
@@ -103,12 +103,14 @@ window.__ModuleLoader__.load({
|
|
|
103
103
|
id: "mid-autumn",
|
|
104
104
|
slot: "h-midautumn",
|
|
105
105
|
asset: "mid-autumn.webp",
|
|
106
|
+
source: "/bfs/new_dyn/42e22801827aff71267b03c95fcfd7834168597.png@1672w.webp",
|
|
106
107
|
color: "#384A77",
|
|
107
108
|
kind: "lunar"
|
|
108
109
|
}, {
|
|
109
110
|
id: "national-day",
|
|
110
111
|
slot: "h-nationalday",
|
|
111
112
|
asset: "national-day.webp",
|
|
113
|
+
source: "/bfs/new_dyn/30e97211189f7869dae83b909d2a3cc84168597.png@3456w.webp",
|
|
112
114
|
color: "#FFF6EB",
|
|
113
115
|
kind: "gregorian"
|
|
114
116
|
}];
|
|
@@ -472,7 +474,9 @@ window.__ModuleLoader__.load({
|
|
|
472
474
|
holidays: {
|
|
473
475
|
enabled: false,
|
|
474
476
|
items: defaultHolidayRules()
|
|
475
|
-
}
|
|
477
|
+
},
|
|
478
|
+
agentAccess: false,
|
|
479
|
+
rev: 0
|
|
476
480
|
};
|
|
477
481
|
}
|
|
478
482
|
/** Slot names reach the filesystem, so they are strictly whitelisted. */
|
|
@@ -738,7 +742,9 @@ window.__ModuleLoader__.load({
|
|
|
738
742
|
rightbarOpacity: normalizeRightbarOpacity(r.rightbarOpacity),
|
|
739
743
|
pageOpacity: clamp01(r.pageOpacity, 0),
|
|
740
744
|
autoExtract: r.autoExtract !== false,
|
|
741
|
-
holidays: normalizeHolidays(r.holidays)
|
|
745
|
+
holidays: normalizeHolidays(r.holidays),
|
|
746
|
+
agentAccess: r.agentAccess === true,
|
|
747
|
+
rev: typeof r.rev === "number" && isFinite(r.rev) && r.rev >= 0 ? Math.floor(r.rev) : 0
|
|
742
748
|
};
|
|
743
749
|
}
|
|
744
750
|
freshThemeConfig();
|
|
@@ -1027,12 +1033,23 @@ window.__ModuleLoader__.load({
|
|
|
1027
1033
|
}
|
|
1028
1034
|
rotIndex = 0;
|
|
1029
1035
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
+
/**
|
|
1037
|
+
* Record the revision the host just wrote.
|
|
1038
|
+
*
|
|
1039
|
+
* Called after EVERY successful write, and it is load-bearing rather than
|
|
1040
|
+
* bookkeeping: the next write names this number as the one it started from, so
|
|
1041
|
+
* leaving it stale makes the very next save look like a lost race — the host
|
|
1042
|
+
* refuses it, hands back the config it already has (which does not contain the
|
|
1043
|
+
* edit), and the panel adopts that. That is the "click + twice" bug: the first
|
|
1044
|
+
* click is refused as stale and rolled back, the second only works once a stale
|
|
1045
|
+
* revision happens to match again.
|
|
1046
|
+
*
|
|
1047
|
+
* Mutated on the live object, never replaced: `cfg` is read through its binding
|
|
1048
|
+
* everywhere, and swapping the reference would strand the modules that hold it.
|
|
1049
|
+
*/
|
|
1050
|
+
function setRev(rev) {
|
|
1051
|
+
if (!isFinite(rev) || rev < 0) return;
|
|
1052
|
+
cfg.rev = Math.floor(rev);
|
|
1036
1053
|
}
|
|
1037
1054
|
//#endregion
|
|
1038
1055
|
//#region src/client/rpc.ts
|
|
@@ -1081,6 +1098,124 @@ window.__ModuleLoader__.load({
|
|
|
1081
1098
|
}
|
|
1082
1099
|
const SAVE_DEBOUNCE_MS = 250;
|
|
1083
1100
|
let saveTimer;
|
|
1101
|
+
/**
|
|
1102
|
+
* Called when a write came back as a conflict, so the plugin can adopt the config
|
|
1103
|
+
* that won and repaint. Set once by `apply()` in ./index.
|
|
1104
|
+
*
|
|
1105
|
+
* A callback rather than an import because `./rpc` owns no state: the memory copy,
|
|
1106
|
+
* the repaint and the toast all live in the modules that own those, and this file
|
|
1107
|
+
* would have to import half the plugin to reach them.
|
|
1108
|
+
*/
|
|
1109
|
+
let onConflict = null;
|
|
1110
|
+
function setConflictHandler(handler) {
|
|
1111
|
+
onConflict = handler;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* One config write, with the conflict case handled rather than logged.
|
|
1115
|
+
*
|
|
1116
|
+
* The generic `rpcCall` answers `undefined` for anything that is not a plain
|
|
1117
|
+
* success, which is right for reads but wrong here: a REFUSED write is not "no
|
|
1118
|
+
* answer", it is an answer that carries the config which won. So this one goes
|
|
1119
|
+
* through `rpcRaw` and unwraps the outcome itself.
|
|
1120
|
+
*/
|
|
1121
|
+
async function writeConfigNow(config) {
|
|
1122
|
+
const res = await rpcRaw("writeConfig", {
|
|
1123
|
+
config,
|
|
1124
|
+
expectedRev: cfg.rev
|
|
1125
|
+
});
|
|
1126
|
+
if (res === void 0) return {
|
|
1127
|
+
ok: false,
|
|
1128
|
+
conflict: false,
|
|
1129
|
+
rev: cfg.rev
|
|
1130
|
+
};
|
|
1131
|
+
if (res.ok !== true) return {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
conflict: false,
|
|
1134
|
+
rev: cfg.rev
|
|
1135
|
+
};
|
|
1136
|
+
const value = res.value;
|
|
1137
|
+
if (value?.ok === true) return {
|
|
1138
|
+
ok: true,
|
|
1139
|
+
conflict: false,
|
|
1140
|
+
rev: typeof value.rev === "number" ? value.rev : cfg.rev
|
|
1141
|
+
};
|
|
1142
|
+
if (value?.conflict === true) return {
|
|
1143
|
+
ok: false,
|
|
1144
|
+
conflict: true,
|
|
1145
|
+
config: value.config,
|
|
1146
|
+
rev: typeof value.rev === "number" ? value.rev : cfg.rev
|
|
1147
|
+
};
|
|
1148
|
+
return {
|
|
1149
|
+
ok: false,
|
|
1150
|
+
conflict: false,
|
|
1151
|
+
rev: cfg.rev
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Persist, and adopt the winner when the write was refused as stale.
|
|
1156
|
+
*
|
|
1157
|
+
* Deliberately NOT retried: the edit that lost was computed from a config that is
|
|
1158
|
+
* no longer there, and replaying it on the winner would apply a change the user
|
|
1159
|
+
* made against a different state. Adopting is the honest answer — the user sees
|
|
1160
|
+
* the new configuration and can repeat the gesture if they still want it.
|
|
1161
|
+
*
|
|
1162
|
+
* The SUCCESS path is the one that has to be careful: a write that lands advances
|
|
1163
|
+
* the revision on disk, so this bundle's own copy must advance with it, or its next
|
|
1164
|
+
* save names a revision that is already gone, gets refused, and adopts the config
|
|
1165
|
+
* from BEFORE that save — which is the edit visibly undoing itself.
|
|
1166
|
+
*/
|
|
1167
|
+
/**
|
|
1168
|
+
* ONE writer at a time, and this is load-bearing rather than tidy.
|
|
1169
|
+
*
|
|
1170
|
+
* Every save names the revision it started from, and a successful one ADVANCES that
|
|
1171
|
+
* revision. Two saves fired back to back (a fast double delete, a drag followed by a
|
|
1172
|
+
* click) therefore both name the SAME revision — the first is accepted, the second is
|
|
1173
|
+
* refused as stale, and the refusal hands back the config from before the second
|
|
1174
|
+
* edit. The panel adopts it, so the second edit disappears from the screen while its
|
|
1175
|
+
* side effects (a picture's bytes, already deleted) do not: that is the card reading
|
|
1176
|
+
* "这张图片还没有取到内容" next to a rule the user just deleted.
|
|
1177
|
+
*
|
|
1178
|
+
* Chaining the writes means the second one starts after the first has recorded its
|
|
1179
|
+
* revision, which is the only state it can be correct against. It is a single chain
|
|
1180
|
+
* for the whole config rather than one per key because the config IS one document —
|
|
1181
|
+
* two concurrent writes to it can never both be right.
|
|
1182
|
+
*/
|
|
1183
|
+
let writeChain = Promise.resolve();
|
|
1184
|
+
/**
|
|
1185
|
+
* Work that must only happen once the config naming it has actually landed.
|
|
1186
|
+
*
|
|
1187
|
+
* Releasing a picture's bytes is the case: the bytes are what the config points AT,
|
|
1188
|
+
* so deleting them first and then losing the config write leaves the rule on screen
|
|
1189
|
+
* with nothing behind it. Queued here instead, a failed or superseded write simply
|
|
1190
|
+
* never runs the delete — the file stays, which is wrong only in that it is
|
|
1191
|
+
* unreferenced, and the cleanup card exists for exactly that.
|
|
1192
|
+
*/
|
|
1193
|
+
let deferredDeletes = [];
|
|
1194
|
+
/**
|
|
1195
|
+
* Delete one slot's bytes, but only after the config that stopped referencing it is
|
|
1196
|
+
* on disk.
|
|
1197
|
+
*/
|
|
1198
|
+
function deleteImageAfterSave(slot) {
|
|
1199
|
+
deferredDeletes.push(slot);
|
|
1200
|
+
}
|
|
1201
|
+
function persistAndResolve() {
|
|
1202
|
+
writeChain = writeChain.then(async () => {
|
|
1203
|
+
const outcome = await writeConfigNow(cfg);
|
|
1204
|
+
if (outcome.ok) {
|
|
1205
|
+
setRev(outcome.rev);
|
|
1206
|
+
const releasing = deferredDeletes;
|
|
1207
|
+
deferredDeletes = [];
|
|
1208
|
+
for (const slot of releasing) deleteImage(slot);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (outcome.conflict && outcome.config !== void 0) {
|
|
1212
|
+
deferredDeletes = [];
|
|
1213
|
+
onConflict?.(outcome.config);
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
return writeChain;
|
|
1217
|
+
}
|
|
1218
|
+
/** Write the current config with the revision this bundle last saw. */
|
|
1084
1219
|
function saveConfig() {
|
|
1085
1220
|
if (!canWriteConfig()) {
|
|
1086
1221
|
holdWrite();
|
|
@@ -1089,7 +1224,7 @@ window.__ModuleLoader__.load({
|
|
|
1089
1224
|
if (saveTimer !== void 0) window.clearTimeout(saveTimer);
|
|
1090
1225
|
saveTimer = window.setTimeout(() => {
|
|
1091
1226
|
saveTimer = void 0;
|
|
1092
|
-
|
|
1227
|
+
persistAndResolve();
|
|
1093
1228
|
}, SAVE_DEBOUNCE_MS);
|
|
1094
1229
|
}
|
|
1095
1230
|
function flushSave() {
|
|
@@ -1100,7 +1235,7 @@ window.__ModuleLoader__.load({
|
|
|
1100
1235
|
holdWrite();
|
|
1101
1236
|
return;
|
|
1102
1237
|
}
|
|
1103
|
-
|
|
1238
|
+
persistAndResolve();
|
|
1104
1239
|
}
|
|
1105
1240
|
/** Persist the current config immediately (no debounce). */
|
|
1106
1241
|
function persistConfig() {
|
|
@@ -1108,7 +1243,19 @@ window.__ModuleLoader__.load({
|
|
|
1108
1243
|
holdWrite();
|
|
1109
1244
|
return;
|
|
1110
1245
|
}
|
|
1111
|
-
|
|
1246
|
+
persistAndResolve();
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Run the queued byte deletions without writing anything.
|
|
1250
|
+
*
|
|
1251
|
+
* The queue is normally drained by the next successful write, and every path that
|
|
1252
|
+
* queues one writes immediately afterwards — so this exists only for the page-hide
|
|
1253
|
+
* flush, where a delete could otherwise outlive the tab that asked for it.
|
|
1254
|
+
*/
|
|
1255
|
+
function drainDeferredDeletes() {
|
|
1256
|
+
const releasing = deferredDeletes;
|
|
1257
|
+
deferredDeletes = [];
|
|
1258
|
+
for (const slot of releasing) deleteImage(slot);
|
|
1112
1259
|
}
|
|
1113
1260
|
/** Load the persisted config plus the list of stored image slots. */
|
|
1114
1261
|
async function loadPersisted() {
|
|
@@ -1117,7 +1264,7 @@ window.__ModuleLoader__.load({
|
|
|
1117
1264
|
const d = data;
|
|
1118
1265
|
const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
|
|
1119
1266
|
const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
|
|
1120
|
-
hostShape = typeof schema === "number" && schema >=
|
|
1267
|
+
hostShape = typeof schema === "number" && schema >= 11 ? "current" : "legacy";
|
|
1121
1268
|
if (hostShape === "legacy") holdWrite();
|
|
1122
1269
|
return {
|
|
1123
1270
|
config: d.config,
|
|
@@ -1125,30 +1272,68 @@ window.__ModuleLoader__.load({
|
|
|
1125
1272
|
schema
|
|
1126
1273
|
};
|
|
1127
1274
|
}
|
|
1275
|
+
/**
|
|
1276
|
+
* The revision of the config on disk, or null when the host could not be asked.
|
|
1277
|
+
*
|
|
1278
|
+
* The whole answer is one number: this runs on a timer, and pulling the config to
|
|
1279
|
+
* find out whether it changed would cost a full read every few seconds for a
|
|
1280
|
+
* question that is almost always "no".
|
|
1281
|
+
*/
|
|
1282
|
+
async function readRev() {
|
|
1283
|
+
const data = await rpcCall("readRev", {});
|
|
1284
|
+
if (data === null || typeof data !== "object") return null;
|
|
1285
|
+
const rev = data.rev;
|
|
1286
|
+
return typeof rev === "number" && isFinite(rev) ? rev : null;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Per-slot queue for the byte operations.
|
|
1290
|
+
*
|
|
1291
|
+
* Slots are RECYCLED — `nextSlot` hands out the first free `mN`, and deleting a
|
|
1292
|
+
* rule frees its numbers immediately — so a fast delete-then-add can name the same
|
|
1293
|
+
* slot twice within one round trip. Those two calls are independent requests on one
|
|
1294
|
+
* connection, which means their order at the host is whatever the transport decides
|
|
1295
|
+
* that moment: the delete can land AFTER the write, leaving the config naming a slot
|
|
1296
|
+
* whose bytes were just removed. The panel then paints a card that reads "这张图片还没有
|
|
1297
|
+
* 取到内容" — the rule is right there, the picture is not, and nothing reports an
|
|
1298
|
+
* error because nothing failed.
|
|
1299
|
+
*
|
|
1300
|
+
* So the operations on ONE slot run in the order they were asked for. Different
|
|
1301
|
+
* slots never wait for each other: a queue per slot, not one global one, because
|
|
1302
|
+
* adding seven pictures at once must not serialise into seven round trips.
|
|
1303
|
+
*/
|
|
1304
|
+
const slotQueues = /* @__PURE__ */ new Map();
|
|
1305
|
+
function enqueueForSlot(slot, run) {
|
|
1306
|
+
const next = (slotQueues.get(slot) ?? Promise.resolve()).then(run, run);
|
|
1307
|
+
slotQueues.set(slot, next);
|
|
1308
|
+
next.finally(() => {
|
|
1309
|
+
if (slotQueues.get(slot) === next) slotQueues.delete(slot);
|
|
1310
|
+
});
|
|
1311
|
+
return next;
|
|
1312
|
+
}
|
|
1128
1313
|
/** Read one rule image as a data URL (null when the slot is empty). */
|
|
1129
1314
|
async function readImage(slot) {
|
|
1130
|
-
const data = await rpcCall("readImage", { slot });
|
|
1315
|
+
const data = await enqueueForSlot(slot, () => rpcCall("readImage", { slot }));
|
|
1131
1316
|
if (data === null || typeof data !== "object") return null;
|
|
1132
1317
|
const url = data.dataUrl;
|
|
1133
1318
|
return typeof url === "string" ? url : null;
|
|
1134
1319
|
}
|
|
1135
1320
|
/** Persist (or clear) one rule image; one-shot, no debounce. */
|
|
1136
1321
|
async function writeImage(slot, dataUrl) {
|
|
1137
|
-
return await rpcCall("writeImage", {
|
|
1322
|
+
return await enqueueForSlot(slot, () => rpcCall("writeImage", {
|
|
1138
1323
|
slot,
|
|
1139
1324
|
dataUrl
|
|
1140
|
-
}) === true;
|
|
1325
|
+
})) === true;
|
|
1141
1326
|
}
|
|
1142
1327
|
/** Remove one rule image from disk. */
|
|
1143
1328
|
async function deleteImage(slot) {
|
|
1144
|
-
return await rpcCall("deleteImage", { slot }) === true;
|
|
1329
|
+
return await enqueueForSlot(slot, () => rpcCall("deleteImage", { slot })) === true;
|
|
1145
1330
|
}
|
|
1146
1331
|
/** Download an image from a network URL into one slot, replacing its bytes. */
|
|
1147
1332
|
async function fetchImageUrl(slot, url) {
|
|
1148
|
-
const res = await rpcRaw("fetchImageUrl", {
|
|
1333
|
+
const res = await enqueueForSlot(slot, () => rpcRaw("fetchImageUrl", {
|
|
1149
1334
|
slot,
|
|
1150
1335
|
url
|
|
1151
|
-
});
|
|
1336
|
+
}));
|
|
1152
1337
|
if (!res) return {
|
|
1153
1338
|
ok: false,
|
|
1154
1339
|
error: "no response"
|
|
@@ -1167,17 +1352,13 @@ window.__ModuleLoader__.load({
|
|
|
1167
1352
|
};
|
|
1168
1353
|
}
|
|
1169
1354
|
/**
|
|
1170
|
-
*
|
|
1355
|
+
* One of the profile's wallpapers, addressed by the slot the built-in config names.
|
|
1171
1356
|
*
|
|
1172
1357
|
* Raw rather than `rpcCall` for the same reason `fetchImageUrl` is: the node half
|
|
1173
|
-
* answers this one with a value that CARRIES its own failure, and the `error`
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1358
|
+
* answers this one with a value that CARRIES its own failure, and the `error` inside
|
|
1359
|
+
* it is the only thing worth showing the user — the generic "rpc failed" log would
|
|
1360
|
+
* throw that distinction away.
|
|
1176
1361
|
*/
|
|
1177
|
-
async function fetchPresetConfig() {
|
|
1178
|
-
return unwrapPreset(await rpcRaw("fetchPresetConfig", {}));
|
|
1179
|
-
}
|
|
1180
|
-
/** One of the profile's wallpapers, addressed by the slot the config names. */
|
|
1181
1362
|
async function fetchPresetImage(slot) {
|
|
1182
1363
|
return unwrapPreset(await rpcRaw("fetchPresetImage", { slot }));
|
|
1183
1364
|
}
|
|
@@ -1202,15 +1383,24 @@ window.__ModuleLoader__.load({
|
|
|
1202
1383
|
};
|
|
1203
1384
|
return v;
|
|
1204
1385
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1386
|
+
async function adoptRecommended() {
|
|
1387
|
+
const res = await rpcRaw("adoptRecommended", {});
|
|
1388
|
+
if (res === void 0 || res.ok !== true) return {
|
|
1389
|
+
ok: false,
|
|
1390
|
+
rev: cfg.rev
|
|
1391
|
+
};
|
|
1392
|
+
const value = res.value;
|
|
1393
|
+
const base = {
|
|
1394
|
+
ok: value?.ok === true,
|
|
1395
|
+
rev: typeof value?.rev === "number" ? value.rev : cfg.rev
|
|
1396
|
+
};
|
|
1397
|
+
if (base.ok) return base;
|
|
1398
|
+
return {
|
|
1399
|
+
...base,
|
|
1400
|
+
error: typeof value?.error === "string" ? value.error : void 0,
|
|
1401
|
+
slot: typeof value?.slot === "string" ? value.slot : void 0,
|
|
1402
|
+
wiped: value?.wiped === true
|
|
1403
|
+
};
|
|
1214
1404
|
}
|
|
1215
1405
|
/** Host default model — used only when the per-session services are absent. */
|
|
1216
1406
|
async function readDefaultModel() {
|
|
@@ -3682,8 +3872,9 @@ window.__ModuleLoader__.load({
|
|
|
3682
3872
|
*
|
|
3683
3873
|
* Every interval the plugin owns is declared HERE rather than at its call site, so
|
|
3684
3874
|
* "what does this plugin poll, and how often" has one answer. The scheduler itself
|
|
3685
|
-
* lives in ./watchdogs; the questions live in their own modules
|
|
3686
|
-
* the
|
|
3875
|
+
* lives in ./watchdogs; the questions live in their own modules — the theme
|
|
3876
|
+
* watchdog and the holiday rollover in ./appearance-checks, the delayed boot
|
|
3877
|
+
* restores in ./appearance.
|
|
3687
3878
|
*/
|
|
3688
3879
|
/** The theme skin / host palette re-assert, once a second while a colour is live. */
|
|
3689
3880
|
const THEME_WATCH_MS = 1e3;
|
|
@@ -3695,6 +3886,16 @@ window.__ModuleLoader__.load({
|
|
|
3695
3886
|
const RESTORE_DELAYS_MS = [300, 1500];
|
|
3696
3887
|
/** The holiday rollover: the answer only moves at Beijing midnight, a whole day wide. */
|
|
3697
3888
|
const HOLIDAY_WATCH_MS = 3e4;
|
|
3889
|
+
/**
|
|
3890
|
+
* How often the panel checks whether the config on disk moved under it.
|
|
3891
|
+
*
|
|
3892
|
+
* Only matters once agent access is on (the tools are the other writer), so the
|
|
3893
|
+
* check is a conditional no-op the rest of the time: it asks the host for one
|
|
3894
|
+
* number and stops. 3 s is the compromise between noticing an agent's edit while
|
|
3895
|
+
* the user is still looking at the screen and not paying a request per second for
|
|
3896
|
+
* a file that usually has not changed.
|
|
3897
|
+
*/
|
|
3898
|
+
const REV_POLL_MS = 3e3;
|
|
3698
3899
|
//#endregion
|
|
3699
3900
|
//#region src/client/appearance.ts
|
|
3700
3901
|
function createAppearance(host) {
|
|
@@ -4689,9 +4890,13 @@ window.__ModuleLoader__.load({
|
|
|
4689
4890
|
if (url !== null) images[slot] = url;
|
|
4690
4891
|
}
|
|
4691
4892
|
return {
|
|
4692
|
-
version:
|
|
4893
|
+
version: 11,
|
|
4693
4894
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4694
|
-
config:
|
|
4895
|
+
config: {
|
|
4896
|
+
...cfg,
|
|
4897
|
+
agentAccess: false,
|
|
4898
|
+
rev: 0
|
|
4899
|
+
},
|
|
4695
4900
|
images
|
|
4696
4901
|
};
|
|
4697
4902
|
}
|
|
@@ -4721,20 +4926,24 @@ window.__ModuleLoader__.load({
|
|
|
4721
4926
|
* Write a whole theme file into the store: its rules, its wallpapers, nothing
|
|
4722
4927
|
* else.
|
|
4723
4928
|
*
|
|
4724
|
-
*
|
|
4725
|
-
*
|
|
4726
|
-
*
|
|
4727
|
-
*
|
|
4728
|
-
*
|
|
4929
|
+
* The recommended profile no longer comes through here. It used to, as a
|
|
4930
|
+
* `wipeFirst` variant of this function, and that was the shape of a real bug: the
|
|
4931
|
+
* wipe is a separate round trip that empties the config ON DISK, so the write this
|
|
4932
|
+
* function then made named a revision the emptied store had never heard of, was
|
|
4933
|
+
* refused as stale, and adopted the config it had just replaced — the button had to
|
|
4934
|
+
* be pressed twice. Installing the profile is now one node-half operation
|
|
4935
|
+
* (`adoptRecommended`), and this path only ever imports a file over a live store.
|
|
4729
4936
|
*/
|
|
4730
|
-
async function applyThemeFile(host, data
|
|
4731
|
-
if (typeof data.version === "number" && data.version >
|
|
4937
|
+
async function applyThemeFile(host, data) {
|
|
4938
|
+
if (typeof data.version === "number" && data.version > 11) return false;
|
|
4732
4939
|
if (typeof data.config !== "object" || data.config === null) return false;
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4940
|
+
const ownRev = cfg.rev;
|
|
4941
|
+
const ownAccess = cfg.agentAccess;
|
|
4942
|
+
adoptConfig({
|
|
4943
|
+
...data.config,
|
|
4944
|
+
agentAccess: ownAccess,
|
|
4945
|
+
rev: ownRev
|
|
4946
|
+
});
|
|
4738
4947
|
const incoming = data.images ?? {};
|
|
4739
4948
|
const keep = /* @__PURE__ */ new Set();
|
|
4740
4949
|
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
@@ -4745,10 +4954,10 @@ window.__ModuleLoader__.load({
|
|
|
4745
4954
|
writeImage(slot, raw);
|
|
4746
4955
|
} else {
|
|
4747
4956
|
setImage(slot, null);
|
|
4748
|
-
|
|
4957
|
+
deleteImageAfterSave(slot);
|
|
4749
4958
|
}
|
|
4750
4959
|
}
|
|
4751
|
-
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && SLOT_RE.test(slot))
|
|
4960
|
+
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && SLOT_RE.test(slot)) deleteImageAfterSave(slot);
|
|
4752
4961
|
persistConfig();
|
|
4753
4962
|
host.applyActive();
|
|
4754
4963
|
return true;
|
|
@@ -4970,7 +5179,7 @@ window.__ModuleLoader__.load({
|
|
|
4970
5179
|
importDone: "已导入配置",
|
|
4971
5180
|
importFail: "导入失败,文件格式不正确",
|
|
4972
5181
|
recCardTitle: "推荐配置",
|
|
4973
|
-
recCardDesc: "
|
|
5182
|
+
recCardDesc: "换上作者维护的一整套配置(规则与界面数值随插件内置,壁纸从图床下载),一键替换当前的全部内容",
|
|
4974
5183
|
recAdopt: "采用推荐配置",
|
|
4975
5184
|
recBusy: "正在下载",
|
|
4976
5185
|
recConfirmTitle: "替换当前的全部配置?",
|
|
@@ -4980,13 +5189,12 @@ window.__ModuleLoader__.load({
|
|
|
4980
5189
|
recConfirmOk: "删除并替换",
|
|
4981
5190
|
recConfirmCancel: "取消",
|
|
4982
5191
|
recDone: "已采用推荐配置",
|
|
4983
|
-
recFailNetwork: "
|
|
4984
|
-
recFailNotProfile: "素材库返回的内容不是配置,请稍后重试",
|
|
4985
|
-
recFailNewer: "这份推荐配置需要更新的插件版本,未做任何改动",
|
|
4986
|
-
recFailTooMany: "这份推荐配置列出的图片数量超出限制,未做任何改动",
|
|
5192
|
+
recFailNetwork: "壁纸下载失败,请检查网络后重试(已下到的会留到下一次)",
|
|
4987
5193
|
recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
|
|
4988
|
-
recFailImage: "
|
|
5194
|
+
recFailImage: "这张壁纸没拿到(图床上这个地址没有回应):",
|
|
4989
5195
|
recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
|
|
5196
|
+
recFailAfterWipe: "推荐配置写了一半就失败了,你原来的规则已经被清空(图片也一起没了)。再点一次「使用推荐配置」可以重来,图片是重新下载的",
|
|
5197
|
+
recFailNoStore: "壁纸没能下载,推荐配置尚未开始,你原来的配置没有被改动",
|
|
4990
5198
|
storeCardTitle: "图片存储",
|
|
4991
5199
|
storeCardDesc: "因意外导致的孤儿图片这里可以清掉",
|
|
4992
5200
|
storeOrphans: "可清理",
|
|
@@ -5003,6 +5211,9 @@ window.__ModuleLoader__.load({
|
|
|
5003
5211
|
repoLink: "在 GitHub 上打开仓库",
|
|
5004
5212
|
holidayTitle: "节日特殊背景",
|
|
5005
5213
|
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)",
|
|
5214
|
+
agentTitle: "允许 AI 修改背景",
|
|
5215
|
+
agentEnable: "打开后,AI 可以调用工具增删改规则、切图、调透明度/颜色/模糊/布局。关闭时这些工具对 AI 完全不可见,不占用上下文。",
|
|
5216
|
+
agentOn: "已开启:AI 可以修改这套外观,改动会即时生效。",
|
|
5006
5217
|
trTitle: "切换效果",
|
|
5007
5218
|
trEffect: "切换方式",
|
|
5008
5219
|
trEffectFade: "淡入淡出",
|
|
@@ -5154,7 +5365,7 @@ window.__ModuleLoader__.load({
|
|
|
5154
5365
|
importDone: "Profile imported",
|
|
5155
5366
|
importFail: "Import failed — invalid file",
|
|
5156
5367
|
recCardTitle: "Recommended profile",
|
|
5157
|
-
recCardDesc: "
|
|
5368
|
+
recCardDesc: "Install the author's own full setup — rules and interface numbers ship with the plugin, wallpapers are downloaded from the image host — replacing everything you have",
|
|
5158
5369
|
recAdopt: "Use recommended profile",
|
|
5159
5370
|
recBusy: "Downloading",
|
|
5160
5371
|
recConfirmTitle: "Replace your entire profile?",
|
|
@@ -5164,13 +5375,12 @@ window.__ModuleLoader__.load({
|
|
|
5164
5375
|
recConfirmOk: "Delete and replace",
|
|
5165
5376
|
recConfirmCancel: "Cancel",
|
|
5166
5377
|
recDone: "Recommended profile applied",
|
|
5167
|
-
recFailNetwork: "Could not download the
|
|
5168
|
-
recFailNotProfile: "The asset host answered with something that is not a profile — try again later",
|
|
5169
|
-
recFailNewer: "This profile needs a newer plugin version. Nothing was changed.",
|
|
5170
|
-
recFailTooMany: "The recommended profile names more images than the plugin will fetch. Nothing was changed.",
|
|
5378
|
+
recFailNetwork: "Could not download the wallpapers — check your connection and retry (what arrived is kept for the next try)",
|
|
5171
5379
|
recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
|
|
5172
|
-
recFailImage: "Could not fetch this wallpaper — the
|
|
5380
|
+
recFailImage: "Could not fetch this wallpaper — the image host does not answer for that address:",
|
|
5173
5381
|
recFailApply: "Could not write the recommended profile — your own is untouched",
|
|
5382
|
+
recFailAfterWipe: "The recommended profile failed halfway, after your own rules had already been cleared (their pictures too). Pressing it again starts over, and the wallpapers are re-downloaded",
|
|
5383
|
+
recFailNoStore: "The wallpapers could not be downloaded, so nothing was installed — your own config is untouched",
|
|
5174
5384
|
storeCardTitle: "Image storage",
|
|
5175
5385
|
storeCardDesc: "Orphaned image files left behind by an accident can be cleared here",
|
|
5176
5386
|
storeOrphans: "Cleanable",
|
|
@@ -5187,6 +5397,9 @@ window.__ModuleLoader__.load({
|
|
|
5187
5397
|
repoLink: "Open the repository on GitHub",
|
|
5188
5398
|
holidayTitle: "Holiday backgrounds",
|
|
5189
5399
|
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)",
|
|
5400
|
+
agentTitle: "Let the AI change the background",
|
|
5401
|
+
agentEnable: "When on, the AI can add, edit and remove rules, switch images, and set opacity, colour, blur and layout. When off these tools are entirely absent from the AI, costing no context.",
|
|
5402
|
+
agentOn: "On: the AI can change this appearance, and edits take effect immediately.",
|
|
5190
5403
|
trTitle: "Switch effect",
|
|
5191
5404
|
trEffect: "Transition",
|
|
5192
5405
|
trEffectFade: "Cross-fade",
|
|
@@ -5206,41 +5419,6 @@ window.__ModuleLoader__.load({
|
|
|
5206
5419
|
//#endregion
|
|
5207
5420
|
//#region src/preset.ts
|
|
5208
5421
|
/**
|
|
5209
|
-
* JSON with object keys in a fixed (sorted) order — the same string for the same
|
|
5210
|
-
* value, whatever order the object that carried it happened to build its keys in.
|
|
5211
|
-
*
|
|
5212
|
-
* `JSON.stringify` alone is not an identity for a value that came off the wire and
|
|
5213
|
-
* through a sanitizer: key order is an accident of how the object was assembled,
|
|
5214
|
-
* and a resume that missed on it would silently re-download everything (the safe
|
|
5215
|
-
* direction) while a resume that MATCHED on a value it should not have would be
|
|
5216
|
-
* the unsafe one — so this is deliberately total as well: anything JSON cannot
|
|
5217
|
-
* carry (a function, a `Symbol`, `undefined`) serializes as `null` and an
|
|
5218
|
-
* `undefined` property is skipped, exactly as `JSON.stringify` treats it.
|
|
5219
|
-
*/
|
|
5220
|
-
function canonical(value) {
|
|
5221
|
-
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
5222
|
-
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
5223
|
-
return JSON.stringify(value) ?? "null";
|
|
5224
|
-
}
|
|
5225
|
-
/**
|
|
5226
|
-
* The identity of one revision of the profile: the manifest's shape version plus
|
|
5227
|
-
* the config, in canonical form.
|
|
5228
|
-
*
|
|
5229
|
-
* The profile is served at ONE version of the assets repository, but a version
|
|
5230
|
-
* reference is only as fixed as the tag behind it: re-point the tag and every
|
|
5231
|
-
* edge that has not been purged keeps answering with the previous bytes of the
|
|
5232
|
-
* same URL. So "the profile" is still not guaranteed to be one fixed thing, and
|
|
5233
|
-
* the bytes a previous attempt is holding are only known to belong to what is
|
|
5234
|
-
* being installed when that config comes back the same. Order-insensitivity is
|
|
5235
|
-
* what lets this be a comparison of VALUES rather than of spellings —
|
|
5236
|
-
* `normalizeConfig` (./schema) rebuilds the config it reads, so pinning the key
|
|
5237
|
-
* to the exact key order the wire happened to use would make the hold useless
|
|
5238
|
-
* the moment anything upstream normalized differently.
|
|
5239
|
-
*/
|
|
5240
|
-
function presetKey(version, config) {
|
|
5241
|
-
return `${version}\n${canonical(config)}`;
|
|
5242
|
-
}
|
|
5243
|
-
/**
|
|
5244
5422
|
* The bytes of a failed attempt that may be reused for the revision `key` names.
|
|
5245
5423
|
*
|
|
5246
5424
|
* A fresh object every time, never the held record itself: the caller fills it in
|
|
@@ -5264,6 +5442,147 @@ window.__ModuleLoader__.load({
|
|
|
5264
5442
|
function presetMissing(slots, images) {
|
|
5265
5443
|
return slots.filter((slot) => images[slot] === void 0);
|
|
5266
5444
|
}
|
|
5445
|
+
/** Every slot the profile installs, in list order — the download set. */
|
|
5446
|
+
const PROFILE_SLOTS = [
|
|
5447
|
+
{
|
|
5448
|
+
slot: "m1",
|
|
5449
|
+
path: "/bfs/new_dyn/e4565384e7e5b2d75756e577f505534b4168597.png"
|
|
5450
|
+
},
|
|
5451
|
+
{
|
|
5452
|
+
slot: "m2",
|
|
5453
|
+
path: "/bfs/new_dyn/aa6e8649701c91e5ac8f43ff28b97a924168597.png"
|
|
5454
|
+
},
|
|
5455
|
+
{
|
|
5456
|
+
slot: "m3",
|
|
5457
|
+
path: "/bfs/new_dyn/5da92b31df5ee2a20b17ec6be353cf5c4168597.png"
|
|
5458
|
+
},
|
|
5459
|
+
{
|
|
5460
|
+
slot: "m4",
|
|
5461
|
+
path: "/bfs/new_dyn/14bb1e53cb2c13ca80bd23e3c1d2a04e4168597.png"
|
|
5462
|
+
},
|
|
5463
|
+
{
|
|
5464
|
+
slot: "m5",
|
|
5465
|
+
path: "/bfs/new_dyn/f51fff40de2d6eaa5a73d177f7bbfa794168597.png"
|
|
5466
|
+
},
|
|
5467
|
+
{
|
|
5468
|
+
slot: "m6",
|
|
5469
|
+
path: "/bfs/new_dyn/cfa71d4e60a2f12aaeec3efe1aa680b34168597.png"
|
|
5470
|
+
},
|
|
5471
|
+
{
|
|
5472
|
+
slot: "m7",
|
|
5473
|
+
path: "/bfs/new_dyn/c09d34b79d9b447d7db98da68b97afc54168597.png"
|
|
5474
|
+
}
|
|
5475
|
+
].map((i) => i.slot);
|
|
5476
|
+
/**
|
|
5477
|
+
* Identity of this generation of the built-in profile.
|
|
5478
|
+
*
|
|
5479
|
+
* Only one thing reads it: what a failed download KEEPS for the next press
|
|
5480
|
+
* (`PresetHeld.key` in ./preset). Editing the rules or the pictures below must bump
|
|
5481
|
+
* it, or a retry could staple a wallpaper from the old set to a config from the new
|
|
5482
|
+
* one — the same reason the remote profile was compared by value rather than
|
|
5483
|
+
* assumed. Nothing else in the plugin persists it.
|
|
5484
|
+
*/
|
|
5485
|
+
const RECOMMENDED_REVISION = "builtin-1";
|
|
5486
|
+
/**
|
|
5487
|
+
* One rule of the profile: the match string, its picture and that picture's color.
|
|
5488
|
+
*
|
|
5489
|
+
* The color is a per-image value (schema 10), and the triple here is the one the
|
|
5490
|
+
* profile has always carried — an HSL accent per model, used to tint the interface
|
|
5491
|
+
* while its wallpaper is on screen.
|
|
5492
|
+
*/
|
|
5493
|
+
function profileRule(id, match, slot, color) {
|
|
5494
|
+
return {
|
|
5495
|
+
id,
|
|
5496
|
+
images: [defaultImage(slot, color, "fill")],
|
|
5497
|
+
match,
|
|
5498
|
+
enabled: true,
|
|
5499
|
+
color: null,
|
|
5500
|
+
rotate: defaultRotation()
|
|
5501
|
+
};
|
|
5502
|
+
}
|
|
5503
|
+
/**
|
|
5504
|
+
* The profile, as a fresh config every call.
|
|
5505
|
+
*
|
|
5506
|
+
* ORDER IS THE FALLBACK: matching runs top→bottom and the first usable rule is what
|
|
5507
|
+
* an unmatched model gets, so `deepseek` leads — the model this plugin was written
|
|
5508
|
+
* for. Everything below the rules is the interface numbers a fresh install already
|
|
5509
|
+
* starts on (`DEFAULT_PART_OPACITIES` and friends), which is what makes installing
|
|
5510
|
+
* the profile "add the rules and the pictures" on a clean store.
|
|
5511
|
+
*
|
|
5512
|
+
* Deliberately NOT taken from `freshThemeConfig()`: that one seeds a single BLANK
|
|
5513
|
+
* rule, and this one must not inherit it.
|
|
5514
|
+
*/
|
|
5515
|
+
function recommendedConfig() {
|
|
5516
|
+
return {
|
|
5517
|
+
rules: [
|
|
5518
|
+
profileRule("r1", "deepseek", "m3", [
|
|
5519
|
+
191.88931210132625,
|
|
5520
|
+
.5046198735613555,
|
|
5521
|
+
.82
|
|
5522
|
+
]),
|
|
5523
|
+
profileRule("r2", "gpt", "m1", [
|
|
5524
|
+
263.5122459614382,
|
|
5525
|
+
.31076923076923096,
|
|
5526
|
+
.82
|
|
5527
|
+
]),
|
|
5528
|
+
profileRule("r3", "claude", "m2", [
|
|
5529
|
+
18.923814597762384,
|
|
5530
|
+
.5486699795381468,
|
|
5531
|
+
.6
|
|
5532
|
+
]),
|
|
5533
|
+
profileRule("r4", "qwen", "m4", [
|
|
5534
|
+
26.25977410947,
|
|
5535
|
+
.6228354978354975,
|
|
5536
|
+
.82
|
|
5537
|
+
]),
|
|
5538
|
+
profileRule("r5", "glm", "m5", [
|
|
5539
|
+
30.15559737458117,
|
|
5540
|
+
.6552284141830319,
|
|
5541
|
+
.7646280172718907
|
|
5542
|
+
]),
|
|
5543
|
+
profileRule("r6", "kimi", "m6", [
|
|
5544
|
+
229.39645097591443,
|
|
5545
|
+
.5752269829617896,
|
|
5546
|
+
.2
|
|
5547
|
+
]),
|
|
5548
|
+
profileRule("r7", "grok", "m7", [
|
|
5549
|
+
217.3791348600509,
|
|
5550
|
+
.7702645947081058,
|
|
5551
|
+
.2
|
|
5552
|
+
])
|
|
5553
|
+
],
|
|
5554
|
+
opacities: { ...DEFAULT_PART_OPACITIES },
|
|
5555
|
+
transition: defaultTransition(),
|
|
5556
|
+
settingsOpacity: 1,
|
|
5557
|
+
chatTextOpacity: DEFAULT_CHAT_TEXT_OPACITY,
|
|
5558
|
+
trajectoryOpacity: DEFAULT_TRAJECTORY_OPACITY,
|
|
5559
|
+
rightbarOpacity: DEFAULT_RIGHTBAR_OPACITY,
|
|
5560
|
+
pageOpacity: 0,
|
|
5561
|
+
autoExtract: true,
|
|
5562
|
+
holidays: {
|
|
5563
|
+
enabled: true,
|
|
5564
|
+
items: defaultHolidayRules()
|
|
5565
|
+
},
|
|
5566
|
+
agentAccess: false,
|
|
5567
|
+
rev: 0
|
|
5568
|
+
};
|
|
5569
|
+
}
|
|
5570
|
+
/**
|
|
5571
|
+
* The profile as the document both imports consume — the same shape `exportTheme`
|
|
5572
|
+
* writes, so everything downstream (the wipe, the slot loop) is the hand-picked
|
|
5573
|
+
* file's path.
|
|
5574
|
+
*
|
|
5575
|
+
* `images` is filled by the caller as the downloads land, which is what makes a
|
|
5576
|
+
* partially downloaded profile a resumable one rather than an all-or-nothing press.
|
|
5577
|
+
*/
|
|
5578
|
+
function recommendedThemeFile(images = {}) {
|
|
5579
|
+
return {
|
|
5580
|
+
version: 11,
|
|
5581
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5582
|
+
config: recommendedConfig(),
|
|
5583
|
+
images
|
|
5584
|
+
};
|
|
5585
|
+
}
|
|
5267
5586
|
//#endregion
|
|
5268
5587
|
//#region src/client/repaint.ts
|
|
5269
5588
|
/**
|
|
@@ -7722,19 +8041,15 @@ window.__ModuleLoader__.load({
|
|
|
7722
8041
|
/**
|
|
7723
8042
|
* The ways the recommended-profile download can fail, mapped to their copy.
|
|
7724
8043
|
*
|
|
7725
|
-
*
|
|
7726
|
-
*
|
|
7727
|
-
*
|
|
7728
|
-
*
|
|
7729
|
-
*
|
|
7730
|
-
*
|
|
7731
|
-
* one is the author's file naming, and only the slot tells them which.
|
|
8044
|
+
* Two cases, because the profile is now only PICTURES: the rules and their numbers
|
|
8045
|
+
* are compiled in, so there is no manifest to be malformed, no shape to be too new
|
|
8046
|
+
* and no config to name too many slots. What is left is the network ("retry") and
|
|
8047
|
+
* the cap on the total ("nobody should install this"). A failure that names a SLOT
|
|
8048
|
+
* is rendered separately — that one is about one BFS address, and the slot is what
|
|
8049
|
+
* says which.
|
|
7732
8050
|
*/
|
|
7733
8051
|
const REC_ERROR_KEYS = {
|
|
7734
8052
|
"download failed": "recFailNetwork",
|
|
7735
|
-
"not a profile": "recFailNotProfile",
|
|
7736
|
-
"newer version": "recFailNewer",
|
|
7737
|
-
"too many images": "recFailTooMany",
|
|
7738
8053
|
"too large": "recFailTooLarge"
|
|
7739
8054
|
};
|
|
7740
8055
|
function ProfilePage({ p, notify }) {
|
|
@@ -7742,6 +8057,7 @@ window.__ModuleLoader__.load({
|
|
|
7742
8057
|
const importRef = (0, react.useRef)(null);
|
|
7743
8058
|
p.useStore((s) => s.rev + s.rulesRev);
|
|
7744
8059
|
const holidays = cfg.holidays;
|
|
8060
|
+
const agentAccess = cfg.agentAccess;
|
|
7745
8061
|
const tr = cfg.transition;
|
|
7746
8062
|
const store = p.useStore((s) => s);
|
|
7747
8063
|
const [text, setText] = (0, react.useState)("");
|
|
@@ -7825,7 +8141,7 @@ window.__ModuleLoader__.load({
|
|
|
7825
8141
|
});
|
|
7826
8142
|
setRecBusy(false);
|
|
7827
8143
|
if (!res.ok) {
|
|
7828
|
-
notify(res.slot === void 0 ? t(REC_ERROR_KEYS[res.error] ?? "recFailNetwork") : `${t("recFailImage")} ${res.slot}`, false);
|
|
8144
|
+
notify(res.slot === void 0 ? `${t(REC_ERROR_KEYS[res.error] ?? "recFailNetwork")} ${t("recFailNoStore")}` : `${t("recFailImage")} ${res.slot} ${t("recFailNoStore")}`, false);
|
|
7829
8145
|
return;
|
|
7830
8146
|
}
|
|
7831
8147
|
setPending(res.preset);
|
|
@@ -7834,10 +8150,14 @@ window.__ModuleLoader__.load({
|
|
|
7834
8150
|
const preset = pending;
|
|
7835
8151
|
if (preset === null) return;
|
|
7836
8152
|
setRecBusy(true);
|
|
7837
|
-
const
|
|
8153
|
+
const res = await p.applyRecommended(preset);
|
|
7838
8154
|
setRecBusy(false);
|
|
7839
8155
|
setPending(null);
|
|
7840
|
-
|
|
8156
|
+
if (res.ok) {
|
|
8157
|
+
notify(t("recDone"));
|
|
8158
|
+
return;
|
|
8159
|
+
}
|
|
8160
|
+
notify(res.wiped ? t("recFailAfterWipe") : t("recFailApply"), false);
|
|
7841
8161
|
};
|
|
7842
8162
|
const [usage, setUsage] = (0, react.useState)(null);
|
|
7843
8163
|
/** Distinguishes "not asked yet" from "asked, and the host cannot answer". */
|
|
@@ -7898,6 +8218,30 @@ window.__ModuleLoader__.load({
|
|
|
7898
8218
|
})
|
|
7899
8219
|
]
|
|
7900
8220
|
}),
|
|
8221
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
8222
|
+
className: "dab-card dab-rise",
|
|
8223
|
+
style: { "--d": 1 },
|
|
8224
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8225
|
+
className: "dab-chip-row",
|
|
8226
|
+
style: { alignItems: "center" },
|
|
8227
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8228
|
+
type: "button",
|
|
8229
|
+
className: `dab-toggle${agentAccess ? " is-on" : ""}`,
|
|
8230
|
+
role: "switch",
|
|
8231
|
+
"aria-checked": agentAccess,
|
|
8232
|
+
title: t("agentEnable"),
|
|
8233
|
+
onClick: () => {
|
|
8234
|
+
const next = !agentAccess;
|
|
8235
|
+
p.setAgentAccess(next);
|
|
8236
|
+
if (next) notify(t("agentOn"));
|
|
8237
|
+
},
|
|
8238
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
8239
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8240
|
+
className: "dab-holiday-label",
|
|
8241
|
+
children: t("agentTitle")
|
|
8242
|
+
})]
|
|
8243
|
+
})
|
|
8244
|
+
}),
|
|
7901
8245
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
7902
8246
|
className: "dab-card dab-rise",
|
|
7903
8247
|
style: { "--d": 1 },
|
|
@@ -8418,12 +8762,12 @@ window.__ModuleLoader__.load({
|
|
|
8418
8762
|
/**
|
|
8419
8763
|
* Ceiling on what one recommended profile may add up to, across every wallpaper.
|
|
8420
8764
|
*
|
|
8421
|
-
* The node half caps
|
|
8422
|
-
*
|
|
8423
|
-
*
|
|
8424
|
-
*
|
|
8425
|
-
*
|
|
8426
|
-
* button press into half a gigabyte
|
|
8765
|
+
* The node half caps ONE file, but it answers one independent call at a time and
|
|
8766
|
+
* never sees a running total — the client is the only side that does, so the total
|
|
8767
|
+
* is bounded here. The real profile is 2.2 MB of images across seven of them, so
|
|
8768
|
+
* this is generous by design: it exists so that a build which grew a wrong URL
|
|
8769
|
+
* (or an edge that served an upload instead of the transform) cannot turn one
|
|
8770
|
+
* button press into half a gigabyte.
|
|
8427
8771
|
*/
|
|
8428
8772
|
const PRESET_TOTAL_MAX = 33554432;
|
|
8429
8773
|
/**
|
|
@@ -8434,7 +8778,8 @@ window.__ModuleLoader__.load({
|
|
|
8434
8778
|
* CDN edge was still cold). What is being paid is per-request latency, and the
|
|
8435
8779
|
* host speaks HTTP/2 — the bytes are identical either way and the connection is
|
|
8436
8780
|
* reused — so overlapping them buys wall-clock time and risks nothing but a
|
|
8437
|
-
* little contention.
|
|
8781
|
+
* little contention. The same holds for the Bilibili edges the pictures come from
|
|
8782
|
+
* now (see `PROFILE_HOSTS` in ../profile).
|
|
8438
8783
|
*
|
|
8439
8784
|
* Four rather than all seven: a burst that wide competes for the same bandwidth
|
|
8440
8785
|
* and makes the per-file timeout harder to reason about, and four already hides
|
|
@@ -8669,6 +9014,22 @@ window.__ModuleLoader__.load({
|
|
|
8669
9014
|
};
|
|
8670
9015
|
schedule.add(themeWatchdog(checkHost));
|
|
8671
9016
|
schedule.add(holidayRollover(checkHost));
|
|
9017
|
+
schedule.add({
|
|
9018
|
+
name: "rev",
|
|
9019
|
+
everyMs: REV_POLL_MS,
|
|
9020
|
+
run() {
|
|
9021
|
+
if (!cfg.agentAccess) return;
|
|
9022
|
+
(async () => {
|
|
9023
|
+
const rev = await readRev();
|
|
9024
|
+
if (rev === null || rev === cfg.rev) return;
|
|
9025
|
+
const persisted = await loadPersisted();
|
|
9026
|
+
if (persisted === null) return;
|
|
9027
|
+
adoptConfig(persisted.config);
|
|
9028
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) if (displayImageOf(slot) === null) await loadSlot(slot);
|
|
9029
|
+
applyActive();
|
|
9030
|
+
})();
|
|
9031
|
+
}
|
|
9032
|
+
});
|
|
8672
9033
|
(async () => {
|
|
8673
9034
|
const persisted = await loadPersisted();
|
|
8674
9035
|
if (persisted !== null) {
|
|
@@ -8680,6 +9041,13 @@ window.__ModuleLoader__.load({
|
|
|
8680
9041
|
}
|
|
8681
9042
|
applyActive();
|
|
8682
9043
|
})();
|
|
9044
|
+
setConflictHandler((config) => {
|
|
9045
|
+
adoptConfig(config);
|
|
9046
|
+
(async () => {
|
|
9047
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) if (displayImageOf(slot) === null) await loadSlot(slot);
|
|
9048
|
+
applyActive();
|
|
9049
|
+
})();
|
|
9050
|
+
});
|
|
8683
9051
|
ctx.effect(() => () => {
|
|
8684
9052
|
teardownWp();
|
|
8685
9053
|
}, "dsh-background-by-model: wp cleanup");
|
|
@@ -8778,7 +9146,7 @@ window.__ModuleLoader__.load({
|
|
|
8778
9146
|
for (const slot of ruleSlots(rule)) {
|
|
8779
9147
|
if (stillUsed.has(slot)) continue;
|
|
8780
9148
|
setImage(slot, null);
|
|
8781
|
-
|
|
9149
|
+
deleteImageAfterSave(slot);
|
|
8782
9150
|
}
|
|
8783
9151
|
}
|
|
8784
9152
|
rulesRev++;
|
|
@@ -8836,7 +9204,7 @@ window.__ModuleLoader__.load({
|
|
|
8836
9204
|
persistConfig();
|
|
8837
9205
|
if (!takenSlots().has(slot)) {
|
|
8838
9206
|
setImage(slot, null);
|
|
8839
|
-
|
|
9207
|
+
deleteImageAfterSave(slot);
|
|
8840
9208
|
}
|
|
8841
9209
|
repaintIfMoved(id, winner);
|
|
8842
9210
|
},
|
|
@@ -9040,6 +9408,11 @@ window.__ModuleLoader__.load({
|
|
|
9040
9408
|
sync();
|
|
9041
9409
|
saveConfig();
|
|
9042
9410
|
},
|
|
9411
|
+
setAgentAccess: (v) => {
|
|
9412
|
+
cfg.agentAccess = v;
|
|
9413
|
+
sync();
|
|
9414
|
+
persistConfig();
|
|
9415
|
+
},
|
|
9043
9416
|
setHolidaysEnabled: (v) => {
|
|
9044
9417
|
cfg.holidays.enabled = v;
|
|
9045
9418
|
persistConfig();
|
|
@@ -9054,36 +9427,34 @@ window.__ModuleLoader__.load({
|
|
|
9054
9427
|
try {
|
|
9055
9428
|
const data = JSON.parse(await file.text());
|
|
9056
9429
|
if (!data || typeof data !== "object") return false;
|
|
9057
|
-
return await applyThemeFile({ applyActive }, data
|
|
9430
|
+
return await applyThemeFile({ applyActive }, data);
|
|
9058
9431
|
} catch {
|
|
9059
9432
|
return false;
|
|
9060
9433
|
}
|
|
9061
9434
|
},
|
|
9062
9435
|
/**
|
|
9063
|
-
* Fetch the
|
|
9064
|
-
* separate, because the page has to ask before anything is destroyed and
|
|
9065
|
-
* question should describe what is actually in hand.
|
|
9436
|
+
* Fetch the recommended profile's wallpapers. Downloads only — applying them
|
|
9437
|
+
* is separate, because the page has to ask before anything is destroyed and
|
|
9438
|
+
* the question should describe what is actually in hand.
|
|
9066
9439
|
*
|
|
9067
|
-
* The
|
|
9068
|
-
*
|
|
9069
|
-
*
|
|
9070
|
-
*
|
|
9071
|
-
*
|
|
9440
|
+
* The RULES are not fetched at all: they are compiled into the bundle
|
|
9441
|
+
* (./profile), so this is a set of picture transfers and nothing else. That
|
|
9442
|
+
* is why there is no manifest round, no shape gate and no "not a profile"
|
|
9443
|
+
* failure left — nothing remote can decide what this installs.
|
|
9444
|
+
*
|
|
9445
|
+
* Each slot is one request, up to `PRESET_CONCURRENCY` at a time, and each
|
|
9446
|
+
* file keeps its own host fallback, retry rounds (the node half's business,
|
|
9447
|
+
* ./preset), validation and slot name in a failure.
|
|
9072
9448
|
*
|
|
9073
9449
|
* STRICT: the first wallpaper that cannot be had stops the queue. What had
|
|
9074
9450
|
* already arrived is kept for the next press (`presetHeld` below) — the
|
|
9075
9451
|
* difference between retrying one file and retrying the lot. Nothing is
|
|
9076
|
-
* touched here, so
|
|
9077
|
-
*
|
|
9452
|
+
* touched here, so a wrong URL arrives as an error naming the slot rather
|
|
9453
|
+
* than as a profile quietly missing a picture.
|
|
9078
9454
|
*/
|
|
9079
9455
|
fetchRecommended: async (onProgress) => {
|
|
9080
|
-
const
|
|
9081
|
-
|
|
9082
|
-
ok: false,
|
|
9083
|
-
error: head.error
|
|
9084
|
-
};
|
|
9085
|
-
const slots = head.slots;
|
|
9086
|
-
const key = presetKey(head.version, head.config);
|
|
9456
|
+
const slots = PROFILE_SLOTS;
|
|
9457
|
+
const key = RECOMMENDED_REVISION;
|
|
9087
9458
|
const images = presetResume(presetHeld, key);
|
|
9088
9459
|
let bytes = 0;
|
|
9089
9460
|
for (const url of Object.values(images)) bytes += url.length;
|
|
@@ -9131,18 +9502,46 @@ window.__ModuleLoader__.load({
|
|
|
9131
9502
|
presetHeld = null;
|
|
9132
9503
|
return {
|
|
9133
9504
|
ok: true,
|
|
9134
|
-
preset:
|
|
9135
|
-
version: head.version,
|
|
9136
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9137
|
-
config: head.config,
|
|
9138
|
-
images
|
|
9139
|
-
}
|
|
9505
|
+
preset: recommendedThemeFile(images)
|
|
9140
9506
|
};
|
|
9141
9507
|
},
|
|
9142
9508
|
/** Replace EVERYTHING with a downloaded profile: empty the store, then write it. */
|
|
9143
|
-
applyRecommended: async (
|
|
9144
|
-
if (!canWriteConfig()) return
|
|
9145
|
-
|
|
9509
|
+
applyRecommended: async (_preset) => {
|
|
9510
|
+
if (!canWriteConfig()) return {
|
|
9511
|
+
ok: false,
|
|
9512
|
+
wiped: false
|
|
9513
|
+
};
|
|
9514
|
+
const res = await adoptRecommended();
|
|
9515
|
+
if (!res.ok) {
|
|
9516
|
+
if (res.wiped === true) {
|
|
9517
|
+
const after = await loadPersisted();
|
|
9518
|
+
if (after !== null) {
|
|
9519
|
+
adoptConfig(after.config);
|
|
9520
|
+
clearImages();
|
|
9521
|
+
applyActive();
|
|
9522
|
+
}
|
|
9523
|
+
}
|
|
9524
|
+
return {
|
|
9525
|
+
ok: false,
|
|
9526
|
+
error: res.error,
|
|
9527
|
+
slot: res.slot,
|
|
9528
|
+
wiped: res.wiped === true
|
|
9529
|
+
};
|
|
9530
|
+
}
|
|
9531
|
+
const persisted = await loadPersisted();
|
|
9532
|
+
if (persisted === null) return {
|
|
9533
|
+
ok: false,
|
|
9534
|
+
error: "the profile was installed but could not be read back",
|
|
9535
|
+
wiped: true
|
|
9536
|
+
};
|
|
9537
|
+
adoptConfig(persisted.config);
|
|
9538
|
+
clearImages();
|
|
9539
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) await loadSlot(slot);
|
|
9540
|
+
applyActive();
|
|
9541
|
+
return {
|
|
9542
|
+
ok: true,
|
|
9543
|
+
wiped: false
|
|
9544
|
+
};
|
|
9146
9545
|
},
|
|
9147
9546
|
readStoreUsage: () => readStoreUsage(cfg.rules.flatMap(ruleSlots)),
|
|
9148
9547
|
pruneOrphanImages: async () => {
|
|
@@ -9166,7 +9565,10 @@ window.__ModuleLoader__.load({
|
|
|
9166
9565
|
ctx.effect(() => () => {
|
|
9167
9566
|
disposeThemeResets();
|
|
9168
9567
|
}, "dsh-background-by-model: theme resets watch");
|
|
9169
|
-
const onPageHide = () =>
|
|
9568
|
+
const onPageHide = () => {
|
|
9569
|
+
flushSave();
|
|
9570
|
+
drainDeferredDeletes();
|
|
9571
|
+
};
|
|
9170
9572
|
window.addEventListener("pagehide", onPageHide);
|
|
9171
9573
|
ctx.effect(() => () => window.removeEventListener("pagehide", onPageHide), "dsh-background-by-model: pagehide flush");
|
|
9172
9574
|
}
|