dsh-background-by-model 0.7.6 → 0.7.8
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 +23 -1
- package/CHANGELOG.zh.md +23 -1
- package/README.md +20 -19
- package/README.zh.md +20 -19
- package/lib/client.js +214 -80
- package/lib/index.js +194 -40
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -648,11 +648,11 @@ window.__ModuleLoader__.load({
|
|
|
648
648
|
"input"
|
|
649
649
|
];
|
|
650
650
|
const DEFAULT_PART_OPACITIES = {
|
|
651
|
-
bg:
|
|
652
|
-
sidebar: .
|
|
651
|
+
bg: 0,
|
|
652
|
+
sidebar: .1,
|
|
653
653
|
card: 1,
|
|
654
|
-
code:
|
|
655
|
-
input:
|
|
654
|
+
code: .81,
|
|
655
|
+
input: .88
|
|
656
656
|
};
|
|
657
657
|
/**
|
|
658
658
|
* Interface blur (px, 0..60), global (Interface page):
|
|
@@ -683,7 +683,7 @@ window.__ModuleLoader__.load({
|
|
|
683
683
|
chat: 0,
|
|
684
684
|
trajectory: 0,
|
|
685
685
|
rightbar: 0,
|
|
686
|
-
input:
|
|
686
|
+
input: 60
|
|
687
687
|
};
|
|
688
688
|
/** Longest allowed switch duration; 0 is a hard cut. */
|
|
689
689
|
const TRANSITION_MAX_MS = 3e3;
|
|
@@ -742,6 +742,9 @@ window.__ModuleLoader__.load({
|
|
|
742
742
|
function ruleCanPaint(rule) {
|
|
743
743
|
return rule.enabled && (ruleSlots(rule).length > 0 || rule.color !== null);
|
|
744
744
|
}
|
|
745
|
+
const DEFAULT_CHAT_TEXT_OPACITY = .35;
|
|
746
|
+
const DEFAULT_TRAJECTORY_OPACITY = .73;
|
|
747
|
+
const DEFAULT_RIGHTBAR_OPACITY = .45;
|
|
745
748
|
/**
|
|
746
749
|
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
747
750
|
* null when the string is not a hex color.
|
|
@@ -810,20 +813,58 @@ window.__ModuleLoader__.load({
|
|
|
810
813
|
function defaultHolidayRules() {
|
|
811
814
|
return HOLIDAYS.map(defaultHolidayRule);
|
|
812
815
|
}
|
|
813
|
-
/**
|
|
816
|
+
/**
|
|
817
|
+
* A rule with nothing in it yet: no picture, no match string, no color.
|
|
818
|
+
*
|
|
819
|
+
* Two states need this factory and they are the same state — a fresh install
|
|
820
|
+
* (see `freshThemeConfig` and `normalizeConfig`), and the "add rule" tile one
|
|
821
|
+
* entry further down the list.
|
|
822
|
+
*
|
|
823
|
+
* Such a rule is deliberately valid rather than half-built: it is skipped by
|
|
824
|
+
* matching until it has something of its own to paint (`ruleCanPaint` — a picture,
|
|
825
|
+
* or a theme color), which is what makes a rule nobody has filled in yet invisible
|
|
826
|
+
* instead of letting an empty background steal the fallback from rule 1.
|
|
827
|
+
*
|
|
828
|
+
* It used to be born with an allocated slot and an empty image entry, which is
|
|
829
|
+
* what put a blank tile at position 1 of a new rule's strip — and, worse, stopped
|
|
830
|
+
* the picture the user added next from being the first one (a rule paints its
|
|
831
|
+
* FIRST image). The slot is allocated when the first picture arrives.
|
|
832
|
+
*/
|
|
833
|
+
function blankRule(id) {
|
|
834
|
+
return {
|
|
835
|
+
id,
|
|
836
|
+
images: [],
|
|
837
|
+
match: "",
|
|
838
|
+
enabled: true,
|
|
839
|
+
color: null,
|
|
840
|
+
wallpaperOpacity: 1,
|
|
841
|
+
blur: 0,
|
|
842
|
+
rotate: defaultRotation()
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* A default config with independent nested objects (never hand out the shared ones).
|
|
847
|
+
*
|
|
848
|
+
* The rule list is seeded with one blank rule rather than left empty: "no rules at
|
|
849
|
+
* all" is not a state the panel should ever open in, because the first thing a new
|
|
850
|
+
* user has to do is name a match string and drop a picture in — and the drop target
|
|
851
|
+
* for that is the rule card, which an empty list has no way to show. The empty
|
|
852
|
+
* state is still reachable (deleting the last rule is a thing the user can mean),
|
|
853
|
+
* and is described by `ruleNoneHint`.
|
|
854
|
+
*/
|
|
814
855
|
function freshThemeConfig() {
|
|
815
856
|
return {
|
|
816
|
-
rules: [],
|
|
857
|
+
rules: [blankRule("r1")],
|
|
817
858
|
opacities: { ...DEFAULT_PART_OPACITIES },
|
|
818
859
|
blurs: { ...DEFAULT_PART_BLURS },
|
|
819
860
|
transition: defaultTransition(),
|
|
820
861
|
settingsOpacity: 1,
|
|
821
|
-
chatTextOpacity:
|
|
822
|
-
trajectoryOpacity:
|
|
823
|
-
rightbarOpacity:
|
|
862
|
+
chatTextOpacity: DEFAULT_CHAT_TEXT_OPACITY,
|
|
863
|
+
trajectoryOpacity: DEFAULT_TRAJECTORY_OPACITY,
|
|
864
|
+
rightbarOpacity: DEFAULT_RIGHTBAR_OPACITY,
|
|
824
865
|
autoExtract: true,
|
|
825
866
|
holidays: {
|
|
826
|
-
enabled:
|
|
867
|
+
enabled: false,
|
|
827
868
|
items: defaultHolidayRules()
|
|
828
869
|
}
|
|
829
870
|
};
|
|
@@ -1058,14 +1099,36 @@ window.__ModuleLoader__.load({
|
|
|
1058
1099
|
if (typeof id === "string") stored.set(id, item);
|
|
1059
1100
|
}
|
|
1060
1101
|
return {
|
|
1061
|
-
enabled: r.enabled
|
|
1102
|
+
enabled: typeof r.enabled === "boolean" ? r.enabled : false,
|
|
1062
1103
|
items: HOLIDAYS.map((def) => normalizeHolidayRule(def, stored.get(def.id)))
|
|
1063
1104
|
};
|
|
1064
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* The file-preview panel's own opacity.
|
|
1108
|
+
*
|
|
1109
|
+
* Three states, and the middle one is why this is a function rather than the
|
|
1110
|
+
* one-line fallback every other field gets: a real number is the card's own
|
|
1111
|
+
* value, `null` is "never owned, so follow `opacities.bg`", and a KEY that is not
|
|
1112
|
+
* there at all — a hand-written config, or one written before the option existed
|
|
1113
|
+
* — is the shipped profile's 45% (see DEFAULT_RIGHTBAR_OPACITY).
|
|
1114
|
+
*
|
|
1115
|
+
* `null` has to survive as ITSELF instead of falling through to the default:
|
|
1116
|
+
* every release so far has written it — a panel the user never dragged is
|
|
1117
|
+
* persisted as an explicit `null` — so treating it as "missing" would hand the
|
|
1118
|
+
* shipped 45% to every configuration whose preview panel was following the main
|
|
1119
|
+
* background, a silent change to a setup nobody asked to change. It is also
|
|
1120
|
+
* unreachable from the UI (no control returns to it); it exists for configs that
|
|
1121
|
+
* were written while the shipped default was `null`.
|
|
1122
|
+
*/
|
|
1123
|
+
function normalizeRightbarOpacity(v) {
|
|
1124
|
+
if (v === null) return null;
|
|
1125
|
+
if (typeof v === "number" && isFinite(v)) return clamp01(v, 1);
|
|
1126
|
+
return DEFAULT_RIGHTBAR_OPACITY;
|
|
1127
|
+
}
|
|
1065
1128
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
1066
1129
|
function normalizeConfig(raw) {
|
|
1067
1130
|
const r = raw ?? {};
|
|
1068
|
-
const rules = Array.isArray(r.rules) ? r.rules.map(normalizeRule).filter((x) => x !== null) : [];
|
|
1131
|
+
const rules = Array.isArray(r.rules) ? r.rules.map(normalizeRule).filter((x) => x !== null) : [blankRule("r1")];
|
|
1069
1132
|
const ops = r.opacities ?? {};
|
|
1070
1133
|
const bl = r.blurs ?? {};
|
|
1071
1134
|
const blurs = {};
|
|
@@ -1078,9 +1141,9 @@ window.__ModuleLoader__.load({
|
|
|
1078
1141
|
blurs,
|
|
1079
1142
|
transition: normalizeTransition(r.transition, legacyFadeMs(r.rules)),
|
|
1080
1143
|
settingsOpacity: clamp01(r.settingsOpacity, 1),
|
|
1081
|
-
chatTextOpacity: clamp01(r.chatTextOpacity,
|
|
1082
|
-
trajectoryOpacity: clamp01(r.trajectoryOpacity,
|
|
1083
|
-
rightbarOpacity:
|
|
1144
|
+
chatTextOpacity: clamp01(r.chatTextOpacity, DEFAULT_CHAT_TEXT_OPACITY),
|
|
1145
|
+
trajectoryOpacity: clamp01(r.trajectoryOpacity, DEFAULT_TRAJECTORY_OPACITY),
|
|
1146
|
+
rightbarOpacity: normalizeRightbarOpacity(r.rightbarOpacity),
|
|
1084
1147
|
autoExtract: r.autoExtract !== false,
|
|
1085
1148
|
holidays: normalizeHolidays(r.holidays)
|
|
1086
1149
|
};
|
|
@@ -1223,28 +1286,6 @@ window.__ModuleLoader__.load({
|
|
|
1223
1286
|
function holidayById(id) {
|
|
1224
1287
|
return cfg.holidays.items.find((r) => r.id === id) ?? null;
|
|
1225
1288
|
}
|
|
1226
|
-
/**
|
|
1227
|
-
* A fresh rule, holding NO image yet.
|
|
1228
|
-
*
|
|
1229
|
-
* It used to be born with an allocated slot and an empty entry, which is what put
|
|
1230
|
-
* a blank tile at position 1 of a new rule's strip — and, worse, stopped the
|
|
1231
|
-
* picture the user added next from being the first one (a rule paints its first
|
|
1232
|
-
* image). A rule with no pictures is a valid state: `ruleCanPaint` (./schema) skips
|
|
1233
|
-
* it — it has no image and no color yet — until either arrives, and the slot is
|
|
1234
|
-
* allocated when the first picture does.
|
|
1235
|
-
*/
|
|
1236
|
-
function newRule(id) {
|
|
1237
|
-
return {
|
|
1238
|
-
id,
|
|
1239
|
-
images: [],
|
|
1240
|
-
match: "",
|
|
1241
|
-
enabled: true,
|
|
1242
|
-
color: null,
|
|
1243
|
-
wallpaperOpacity: 1,
|
|
1244
|
-
blur: 0,
|
|
1245
|
-
rotate: defaultRotation()
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
1289
|
/** Every slot any rule owns — the user's rules and the holiday entries alike. */
|
|
1249
1290
|
function takenSlots() {
|
|
1250
1291
|
const taken = /* @__PURE__ */ new Set();
|
|
@@ -1364,10 +1405,10 @@ window.__ModuleLoader__.load({
|
|
|
1364
1405
|
return clamp01(cfg.settingsOpacity, 1);
|
|
1365
1406
|
}
|
|
1366
1407
|
function rChatTextOpacity() {
|
|
1367
|
-
return clamp01(cfg.chatTextOpacity,
|
|
1408
|
+
return clamp01(cfg.chatTextOpacity, DEFAULT_CHAT_TEXT_OPACITY);
|
|
1368
1409
|
}
|
|
1369
1410
|
function rTrajectoryOpacity() {
|
|
1370
|
-
return clamp01(cfg.trajectoryOpacity,
|
|
1411
|
+
return clamp01(cfg.trajectoryOpacity, DEFAULT_TRAJECTORY_OPACITY);
|
|
1371
1412
|
}
|
|
1372
1413
|
/** Own opacity of the file-preview panel; while unowned it IS the main background's. */
|
|
1373
1414
|
function rRightbarOpacity() {
|
|
@@ -1392,6 +1433,64 @@ window.__ModuleLoader__.load({
|
|
|
1392
1433
|
rotIndex = 0;
|
|
1393
1434
|
}
|
|
1394
1435
|
//#endregion
|
|
1436
|
+
//#region src/preset.ts
|
|
1437
|
+
/**
|
|
1438
|
+
* JSON with object keys in a fixed (sorted) order — the same string for the same
|
|
1439
|
+
* value, whatever order the object that carried it happened to build its keys in.
|
|
1440
|
+
*
|
|
1441
|
+
* `JSON.stringify` alone is not an identity for a value that came off the wire and
|
|
1442
|
+
* through a sanitizer: key order is an accident of how the object was assembled,
|
|
1443
|
+
* and a resume that missed on it would silently re-download everything (the safe
|
|
1444
|
+
* direction) while a resume that MATCHED on a value it should not have would be
|
|
1445
|
+
* the unsafe one — so this is deliberately total as well: anything JSON cannot
|
|
1446
|
+
* carry (a function, a `Symbol`, `undefined`) serializes as `null` and an
|
|
1447
|
+
* `undefined` property is skipped, exactly as `JSON.stringify` treats it.
|
|
1448
|
+
*/
|
|
1449
|
+
function canonical(value) {
|
|
1450
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
1451
|
+
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(",")}}`;
|
|
1452
|
+
return JSON.stringify(value) ?? "null";
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* The identity of one revision of the profile: the manifest's shape version plus
|
|
1456
|
+
* the config, in canonical form.
|
|
1457
|
+
*
|
|
1458
|
+
* The profile is served from a branch, so "the profile" is not a fixed thing: the
|
|
1459
|
+
* bytes a previous attempt is holding are only known to belong to what is being
|
|
1460
|
+
* installed when that config comes back the same. Order-insensitivity is what lets
|
|
1461
|
+
* this be a comparison of VALUES rather than of spellings — `normalizeConfig`
|
|
1462
|
+
* (./schema) rebuilds the config it reads, so pinning the key to the exact key
|
|
1463
|
+
* order the wire happened to use would make the hold useless the moment anything
|
|
1464
|
+
* upstream normalized differently.
|
|
1465
|
+
*/
|
|
1466
|
+
function presetKey(version, config) {
|
|
1467
|
+
return `${version}\n${canonical(config)}`;
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* The bytes of a failed attempt that may be reused for the revision `key` names.
|
|
1471
|
+
*
|
|
1472
|
+
* A fresh object every time, never the held record itself: the caller fills it in
|
|
1473
|
+
* as the rest of the profile arrives, and handing out the same mutable record
|
|
1474
|
+
* would let a re-fetch of one file edit the state the NEXT attempt resumes from.
|
|
1475
|
+
* An empty answer — nothing held, or held for a different revision — is the honest
|
|
1476
|
+
* one either way: the pool then asks for every slot the config names.
|
|
1477
|
+
*/
|
|
1478
|
+
function presetResume(held, key) {
|
|
1479
|
+
if (held === null || held.key !== key) return {};
|
|
1480
|
+
return { ...held.images };
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* The slots of `slots` that `images` does not hold yet, in the config's own order.
|
|
1484
|
+
*
|
|
1485
|
+
* Order matters in the only way it can here: it decides which file a worker takes
|
|
1486
|
+
* first, and the files are not the same size. Only names the CONFIG asked for are
|
|
1487
|
+
* ever returned, so a held picture nothing points at — a slot the author removed
|
|
1488
|
+
* from the profile — can never be fetched again or shipped.
|
|
1489
|
+
*/
|
|
1490
|
+
function presetMissing(slots, images) {
|
|
1491
|
+
return slots.filter((slot) => images[slot] === void 0);
|
|
1492
|
+
}
|
|
1493
|
+
//#endregion
|
|
1395
1494
|
//#region src/client/rotation.ts
|
|
1396
1495
|
/** Dwell-time presets the settings UI offers, in ms. */
|
|
1397
1496
|
const ROTATE_PRESETS = [
|
|
@@ -2143,7 +2242,10 @@ window.__ModuleLoader__.load({
|
|
|
2143
2242
|
.dab-brand:hover .dab-brand-name{color:var(--dsw-alias-brand-primary)}
|
|
2144
2243
|
.dab-brand-tile{width:30px;height:30px;flex:none;border-radius:9px;display:grid;place-items:center;color:var(--dsw-alias-label-primary-inverted);background:var(--dsw-alias-brand-primary);box-shadow:0 4px 14px -4px var(--dsw-alias-brand-primary)}
|
|
2145
2244
|
.dab-brand-name{font-size:13px;font-weight:650;letter-spacing:.01em;line-height:1.25}
|
|
2146
|
-
|
|
2245
|
+
/* The weak token was written as quaternary-WITH-a-fallback, so this tag has
|
|
2246
|
+
always painted tertiary: the dead name is dropped rather than being kept as
|
|
2247
|
+
decoration nothing resolves to (check:ui rejects the name outright now). */
|
|
2248
|
+
.dab-brand-tag{font-size:9px;letter-spacing:.16em;font-weight:600;color:var(--dsw-alias-label-tertiary);text-transform:uppercase}
|
|
2147
2249
|
.dab-nav-list{position:relative;display:flex;flex-direction:column;gap:4px}
|
|
2148
2250
|
.dab-nav-ind{position:absolute;left:0;right:0;top:0;height:38px;border-radius:11px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,transparent);transition:transform .38s cubic-bezier(.22,1,.36,1)}
|
|
2149
2251
|
.dab-nav-item{position:relative;z-index:1;display:flex;align-items:center;gap:10px;height:38px;padding:0 12px;border:0;background:none;border-radius:11px;color:var(--dsw-alias-label-tertiary);font-size:13px;cursor:pointer;text-align:left;transition:color .22s ease}
|
|
@@ -2196,8 +2298,20 @@ window.__ModuleLoader__.load({
|
|
|
2196
2298
|
.dab-num::-webkit-outer-spin-button,.dab-num::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
|
2197
2299
|
.dab-num{-moz-appearance:textfield;appearance:textfield}
|
|
2198
2300
|
.dab-urlinput{flex:1;min-width:180px;height:34px;padding:0 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12.5px;outline:none;transition:border-color .2s}
|
|
2199
|
-
.dab-urlinput::placeholder{color:var(--dsw-alias-label-quaternary)}
|
|
2200
2301
|
.dab-urlinput:focus{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
|
|
2302
|
+
/* Placeholder text is the field's PREDICTION, never its content: the weak caption
|
|
2303
|
+
token, one step below the text it sits in, so an empty box cannot read as a
|
|
2304
|
+
filled one. All three inputs that carry one are listed here (the rule's match
|
|
2305
|
+
field lives further down, in the rule card) rather than each deciding for
|
|
2306
|
+
itself what "empty" looks like.
|
|
2307
|
+
This used to name --dsw-alias-label-quaternary, which this harness defines
|
|
2308
|
+
NOWHERE: an undefined token drops the declaration, and the placeholder then
|
|
2309
|
+
INHERITS the input's own label-primary — the brightest text in the panel,
|
|
2310
|
+
inside a field the user has not typed in. check:ui now fails on any --dsw-…
|
|
2311
|
+
name the installed theme does not publish, so that one cannot come back.
|
|
2312
|
+
label-caption is the token PLACEHOLDER_RULE (../wallpaper) already uses for the
|
|
2313
|
+
composer, so the plugin answers this question in one voice. */
|
|
2314
|
+
.dab-num::placeholder,.dab-urlinput::placeholder,.dab-rule-match::placeholder{color:var(--dsw-alias-label-caption)}
|
|
2201
2315
|
/* Prints the current hex on the color itself. Without the value this was an
|
|
2202
2316
|
* empty bordered rectangle sitting in the same column as the H/S/L number
|
|
2203
2317
|
* fields, which made it read as one more — blank — text input; nothing is ever
|
|
@@ -2254,7 +2368,6 @@ window.__ModuleLoader__.load({
|
|
|
2254
2368
|
.dab-part-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}
|
|
2255
2369
|
.dab-part-ico{width:32px;height:32px;flex:none;border-radius:10px;display:grid;place-items:center;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
2256
2370
|
.dab-part-name{font-size:13.5px;font-weight:600}
|
|
2257
|
-
.dab-part-badge{margin-left:auto;font-family:var(--dab-mono);font-size:11px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2);border-radius:99px;padding:3px 9px}
|
|
2258
2371
|
|
|
2259
2372
|
/* ── segmented control ───────────────────────────────────────────────────── */
|
|
2260
2373
|
.dab-seg{position:relative;display:inline-flex;padding:3px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:11px}
|
|
@@ -2383,7 +2496,7 @@ window.__ModuleLoader__.load({
|
|
|
2383
2496
|
.dab-pick-item:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
2384
2497
|
.dab-pick-item.is-active{color:var(--dsw-alias-brand-primary);font-weight:600;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
2385
2498
|
.dab-pick-match{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2386
|
-
.dab-pick-n{flex:none;font-family:var(--
|
|
2499
|
+
.dab-pick-n{flex:none;font-family:var(--dab-mono);font-size:10.5px;opacity:.7}
|
|
2387
2500
|
|
|
2388
2501
|
/* ── one rule editor (the tab panel's content) ─────────────────────────────── */
|
|
2389
2502
|
.dab-rule-panel{min-height:0}
|
|
@@ -2398,7 +2511,6 @@ window.__ModuleLoader__.load({
|
|
|
2398
2511
|
.dab-rule-num{font-family:var(--dab-mono);font-size:11px;font-weight:700;color:var(--dsw-alias-label-tertiary);min-width:16px}
|
|
2399
2512
|
.dab-rule-badge{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent);border-radius:99px;padding:2px 8px}
|
|
2400
2513
|
.dab-rule-match{flex:1;min-width:150px;height:32px;padding:0 11px;border-radius:9px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-family:var(--dab-mono);font-size:12.5px;outline:none;transition:border-color .2s,box-shadow .2s}
|
|
2401
|
-
.dab-rule-match::placeholder{color:var(--dsw-alias-label-quaternary)}
|
|
2402
2514
|
.dab-rule-match:focus{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
|
|
2403
2515
|
.dab-icon-btn{width:28px;height:28px;flex:none;display:grid;place-items:center;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;transition:color .2s,border-color .2s,background .2s}
|
|
2404
2516
|
.dab-icon-btn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-brand-primary)}
|
|
@@ -4573,20 +4685,13 @@ window.__ModuleLoader__.load({
|
|
|
4573
4685
|
children: [
|
|
4574
4686
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4575
4687
|
className: "dab-part-head",
|
|
4576
|
-
children: [
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
children: t(labelKey)
|
|
4584
|
-
}),
|
|
4585
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4586
|
-
className: "dab-part-badge",
|
|
4587
|
-
children: [Math.round(opacity * 100), "%"]
|
|
4588
|
-
})
|
|
4589
|
-
]
|
|
4688
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4689
|
+
className: "dab-part-ico",
|
|
4690
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { size: 16 })
|
|
4691
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4692
|
+
className: "dab-part-name",
|
|
4693
|
+
children: t(labelKey)
|
|
4694
|
+
})]
|
|
4590
4695
|
}),
|
|
4591
4696
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
|
|
4592
4697
|
label: t("uiOpacity"),
|
|
@@ -7775,6 +7880,20 @@ window.__ModuleLoader__.load({
|
|
|
7775
7880
|
applyActive();
|
|
7776
7881
|
return true;
|
|
7777
7882
|
};
|
|
7883
|
+
/**
|
|
7884
|
+
* What the last FAILED profile attempt already had in hand.
|
|
7885
|
+
*
|
|
7886
|
+
* Only a failure leaves anything here — a download that completes is applied or
|
|
7887
|
+
* thrown away whole, and holding megabytes of base64 for a re-click that a
|
|
7888
|
+
* second re-download would settle is not a trade this makes. What it buys is the
|
|
7889
|
+
* one case that had no answer at all before: a profile whose sixth of seven files
|
|
7890
|
+
* failed now costs ONE file on the next press instead of seven.
|
|
7891
|
+
*
|
|
7892
|
+
* Held across section re-injections on purpose (it is declared here, in `apply`,
|
|
7893
|
+
* not inside `sectionInject`): the retry is usually the next click of the same
|
|
7894
|
+
* button, but the panel can be closed and reopened between them.
|
|
7895
|
+
*/
|
|
7896
|
+
let presetHeld = null;
|
|
7778
7897
|
const sectionInject = (actions) => {
|
|
7779
7898
|
bound = actions;
|
|
7780
7899
|
sync();
|
|
@@ -7785,7 +7904,7 @@ window.__ModuleLoader__.load({
|
|
|
7785
7904
|
readModelFacts,
|
|
7786
7905
|
imageOf: (slot) => displayImageOf(slot),
|
|
7787
7906
|
addRule: () => {
|
|
7788
|
-
const rule =
|
|
7907
|
+
const rule = blankRule(nextRuleId());
|
|
7789
7908
|
cfg.rules.push(rule);
|
|
7790
7909
|
rulesRev++;
|
|
7791
7910
|
persistConfig();
|
|
@@ -8120,15 +8239,18 @@ window.__ModuleLoader__.load({
|
|
|
8120
8239
|
* request, up to `PRESET_CONCURRENCY` of them at a time. What the fan-out
|
|
8121
8240
|
* buys is wall-clock time — the real profile measured 8.3 s fetched one at
|
|
8122
8241
|
* a time against 4.8 s with four in flight — while each file keeps its own
|
|
8123
|
-
* mirror fallback, its own
|
|
8242
|
+
* mirror fallback, its own retry rounds (the node half's business, see
|
|
8243
|
+
* ./preset), its own validation, and its own slot name in a failure.
|
|
8124
8244
|
*
|
|
8125
8245
|
* STRICT, and the pool is what makes that a decision rather than a
|
|
8126
8246
|
* formality: the first wallpaper that cannot be had stops the queue, so the
|
|
8127
8247
|
* files nobody has asked for yet are never asked for. Whatever was already
|
|
8128
|
-
* in flight finishes and is thrown away
|
|
8129
|
-
*
|
|
8130
|
-
*
|
|
8131
|
-
*
|
|
8248
|
+
* in flight finishes and is thrown away — but what it FETCHED is kept for the
|
|
8249
|
+
* next press (`presetHeld` below), which is the difference between retrying
|
|
8250
|
+
* one file and retrying the lot. Nothing has been touched at this point, so
|
|
8251
|
+
* "the author renamed a file" arrives as an error naming the file rather
|
|
8252
|
+
* than as a profile that is quietly missing a picture — and that loudness is
|
|
8253
|
+
* the whole reason this feature is allowed to follow a branch.
|
|
8132
8254
|
*/
|
|
8133
8255
|
fetchRecommended: async (onProgress) => {
|
|
8134
8256
|
const head = await fetchPresetConfig();
|
|
@@ -8137,17 +8259,22 @@ window.__ModuleLoader__.load({
|
|
|
8137
8259
|
error: head.error
|
|
8138
8260
|
};
|
|
8139
8261
|
const slots = head.slots;
|
|
8140
|
-
const
|
|
8262
|
+
const key = presetKey(head.version, head.config);
|
|
8263
|
+
const images = presetResume(presetHeld, key);
|
|
8264
|
+
let bytes = 0;
|
|
8265
|
+
for (const url of Object.values(images)) bytes += url.length;
|
|
8266
|
+
const missing = presetMissing(slots, images);
|
|
8267
|
+
/** Files already in hand from the attempt this one is resuming. */
|
|
8268
|
+
const held = slots.length - missing.length;
|
|
8141
8269
|
let next = 0;
|
|
8142
8270
|
let done = 0;
|
|
8143
|
-
let bytes = 0;
|
|
8144
8271
|
/** Latched by the first worker to fail, so the rest stop asking. */
|
|
8145
8272
|
let stopped = false;
|
|
8146
|
-
onProgress?.(
|
|
8273
|
+
onProgress?.(held, slots.length);
|
|
8147
8274
|
const worker = async () => {
|
|
8148
8275
|
for (;;) {
|
|
8149
|
-
if (stopped || next >=
|
|
8150
|
-
const slot =
|
|
8276
|
+
if (stopped || next >= missing.length) return null;
|
|
8277
|
+
const slot = missing[next++];
|
|
8151
8278
|
const got = await fetchPresetImage(slot);
|
|
8152
8279
|
if (!got.ok) {
|
|
8153
8280
|
stopped = true;
|
|
@@ -8156,21 +8283,28 @@ window.__ModuleLoader__.load({
|
|
|
8156
8283
|
slot
|
|
8157
8284
|
};
|
|
8158
8285
|
}
|
|
8159
|
-
|
|
8160
|
-
bytes += got.dataUrl.length;
|
|
8161
|
-
if (bytes > PRESET_TOTAL_MAX) {
|
|
8286
|
+
if (bytes + got.dataUrl.length > PRESET_TOTAL_MAX) {
|
|
8162
8287
|
stopped = true;
|
|
8163
8288
|
return { error: "too large" };
|
|
8164
8289
|
}
|
|
8165
|
-
|
|
8290
|
+
images[slot] = got.dataUrl;
|
|
8291
|
+
bytes += got.dataUrl.length;
|
|
8292
|
+
onProgress?.(held + ++done, slots.length);
|
|
8166
8293
|
}
|
|
8167
8294
|
};
|
|
8168
|
-
const failure = (await Promise.all(Array.from({ length: Math.min(PRESET_CONCURRENCY,
|
|
8169
|
-
if (failure !== null)
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8295
|
+
const failure = (await Promise.all(Array.from({ length: Math.min(PRESET_CONCURRENCY, missing.length) }, worker))).find((r) => r !== null) ?? null;
|
|
8296
|
+
if (failure !== null) {
|
|
8297
|
+
presetHeld = {
|
|
8298
|
+
key,
|
|
8299
|
+
images
|
|
8300
|
+
};
|
|
8301
|
+
return {
|
|
8302
|
+
ok: false,
|
|
8303
|
+
error: failure.error,
|
|
8304
|
+
slot: failure.slot
|
|
8305
|
+
};
|
|
8306
|
+
}
|
|
8307
|
+
presetHeld = null;
|
|
8174
8308
|
return {
|
|
8175
8309
|
ok: true,
|
|
8176
8310
|
preset: {
|