dsh-any-background 0.2.9 → 0.2.10
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/README.md +17 -9
- package/README.zh.md +17 -9
- package/lib/client.js +772 -240
- package/lib/client.js.map +1 -1
- package/lib/index.js +133 -37
- package/package.json +6 -2
package/lib/client.js
CHANGED
|
@@ -12,9 +12,15 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
try {
|
|
13
13
|
return require("@deepseek-ai/dsh-client-store");
|
|
14
14
|
} catch {}
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
return require("@deepseek-ai/dsh-client-runtime/client");
|
|
17
|
+
} catch {
|
|
18
|
+
console.warn("dsh-any-background: no client store module resolved on this host; the settings panel will be disabled");
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
/** null when neither store package resolves on this host (see resolveStore). */
|
|
23
|
+
const defineStore = resolveStore()?.defineStore ?? null;
|
|
18
24
|
//#endregion
|
|
19
25
|
//#region src/client/i18n.ts
|
|
20
26
|
const NS = "settings.anyBg";
|
|
@@ -57,6 +63,9 @@ window.__ModuleLoader__.load({
|
|
|
57
63
|
fontEnabled: "启用字体",
|
|
58
64
|
fontNone: "未设置,使用宿主默认字体",
|
|
59
65
|
fontFail: "字体上传失败,请确认是有效的 ttf/otf/woff/woff2 文件",
|
|
66
|
+
uploadTooLarge: "文件过大,上限 {limit}",
|
|
67
|
+
bgUploadFail: "壁纸上传失败",
|
|
68
|
+
videoUploadFail: "视频上传失败,本次播放不会被保存",
|
|
60
69
|
strokeTitle: "文字描边",
|
|
61
70
|
strokeHint: "按区域为文字添加描边:粗细为 0 表示关闭,「自动」会生成与文字颜色相反的颜色,「主题色」跟随当前主题色。代码块与图标已自动豁免,多色语法不会糊成一团",
|
|
62
71
|
strokeWidth: "描边粗细",
|
|
@@ -243,6 +252,9 @@ window.__ModuleLoader__.load({
|
|
|
243
252
|
fontEnabled: "Font enabled",
|
|
244
253
|
fontNone: "None — host default",
|
|
245
254
|
fontFail: "Upload failed — make sure it is a valid ttf/otf/woff/woff2 file",
|
|
255
|
+
uploadTooLarge: "File is too large (limit {limit})",
|
|
256
|
+
bgUploadFail: "Wallpaper upload failed",
|
|
257
|
+
videoUploadFail: "Video upload failed — this playback will not be saved",
|
|
246
258
|
strokeTitle: "Text outline",
|
|
247
259
|
strokeHint: "Outline text per region: width 0 turns it off, \"auto\" picks the inverse of the text color, and \"theme\" follows the current accent. Code blocks and icons are exempted automatically, so multi-color syntax never smears",
|
|
248
260
|
strokeWidth: "Outline width",
|
|
@@ -1117,36 +1129,73 @@ window.__ModuleLoader__.load({
|
|
|
1117
1129
|
error: v?.error ?? "failed"
|
|
1118
1130
|
};
|
|
1119
1131
|
}
|
|
1132
|
+
/** Map a refused upload response onto an outcome. The body has to be read: the
|
|
1133
|
+
* node half names the reason there, and an oversized transfer answers
|
|
1134
|
+
* `413 { ok:false, error:'too large', limit:'100 MB' }`. Checking `res.ok`
|
|
1135
|
+
* alone threw that away, so an oversized upload surfaced as a bare failure —
|
|
1136
|
+
* or, on the wallpaper path, as nothing at all. */
|
|
1137
|
+
async function refusedUpload(res) {
|
|
1138
|
+
const body = await res.json().catch(() => null);
|
|
1139
|
+
if (body?.error === "too large") return {
|
|
1140
|
+
ok: false,
|
|
1141
|
+
refusal: {
|
|
1142
|
+
kind: "too-large",
|
|
1143
|
+
status: res.status,
|
|
1144
|
+
limit: typeof body.limit === "string" ? body.limit : void 0
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
return {
|
|
1148
|
+
ok: false,
|
|
1149
|
+
refusal: {
|
|
1150
|
+
kind: "http",
|
|
1151
|
+
status: res.status
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
/** Localized description of a refusal for the panel's toast. `fallbackKey` is
|
|
1156
|
+
* the caller's own "…upload failed" string, used for anything that is not a
|
|
1157
|
+
* size refusal. */
|
|
1158
|
+
function uploadRefusalText(o, t, fallbackKey) {
|
|
1159
|
+
if (o.refusal?.kind === "too-large") {
|
|
1160
|
+
const limit = o.refusal.limit ?? "";
|
|
1161
|
+
return t("uploadTooLarge").split("{limit}").join(limit);
|
|
1162
|
+
}
|
|
1163
|
+
if (o.refusal !== void 0) return `${t(fallbackKey)} (http ${o.refusal.status})`;
|
|
1164
|
+
return t(fallbackKey);
|
|
1165
|
+
}
|
|
1120
1166
|
/** Upload a video's raw bytes over HTTP (MIME in Content-Type, body untouched
|
|
1121
1167
|
* — no base64 inflation that would blow the RPC body limit on large clips). */
|
|
1122
1168
|
async function uploadVideo(blob, mime) {
|
|
1123
1169
|
try {
|
|
1124
|
-
|
|
1170
|
+
const res = await fetch(VIDEO_UPLOAD_URL, {
|
|
1125
1171
|
method: "POST",
|
|
1126
1172
|
headers: { "Content-Type": mime || "application/octet-stream" },
|
|
1127
1173
|
body: blob
|
|
1128
|
-
})
|
|
1174
|
+
});
|
|
1175
|
+
return res.ok ? { ok: true } : await refusedUpload(res);
|
|
1129
1176
|
} catch (e) {
|
|
1130
1177
|
console.warn("dsh-any-background: video upload failed", e);
|
|
1131
|
-
return false;
|
|
1178
|
+
return { ok: false };
|
|
1132
1179
|
}
|
|
1133
1180
|
}
|
|
1134
1181
|
/** Upload a wallpaper's raw bytes over HTTP — same streaming model as videos,
|
|
1135
1182
|
* original pixels preserved, zero base64 round-trips. */
|
|
1136
1183
|
async function uploadWallpaper(blob) {
|
|
1137
1184
|
try {
|
|
1138
|
-
|
|
1185
|
+
const res = await fetch(WALLPAPER_UPLOAD_URL, {
|
|
1139
1186
|
method: "POST",
|
|
1140
1187
|
headers: { "Content-Type": blob.type || "image/jpeg" },
|
|
1141
1188
|
body: blob
|
|
1142
|
-
})
|
|
1189
|
+
});
|
|
1190
|
+
return res.ok ? { ok: true } : await refusedUpload(res);
|
|
1143
1191
|
} catch (e) {
|
|
1144
1192
|
console.warn("dsh-any-background: wallpaper upload failed", e);
|
|
1145
|
-
return false;
|
|
1193
|
+
return { ok: false };
|
|
1146
1194
|
}
|
|
1147
1195
|
}
|
|
1148
1196
|
/** Upload a custom font's raw bytes over HTTP; resolves the sniffed MIME on
|
|
1149
|
-
* success
|
|
1197
|
+
* success, or a refusal the caller can report (an oversized font used to be
|
|
1198
|
+
* indistinguishable from a corrupt one). */
|
|
1150
1199
|
async function uploadFont(blob) {
|
|
1151
1200
|
try {
|
|
1152
1201
|
const res = await fetch(FONT_UPLOAD_URL, {
|
|
@@ -1154,12 +1203,15 @@ window.__ModuleLoader__.load({
|
|
|
1154
1203
|
headers: { "Content-Type": blob.type || "application/octet-stream" },
|
|
1155
1204
|
body: blob
|
|
1156
1205
|
});
|
|
1157
|
-
if (!res.ok) return
|
|
1206
|
+
if (!res.ok) return await refusedUpload(res);
|
|
1158
1207
|
const data = await res.json().catch(() => null);
|
|
1159
|
-
return
|
|
1208
|
+
return {
|
|
1209
|
+
ok: true,
|
|
1210
|
+
mime: typeof data?.mime === "string" ? data.mime : "font/ttf"
|
|
1211
|
+
};
|
|
1160
1212
|
} catch (e) {
|
|
1161
1213
|
console.warn("dsh-any-background: font upload failed", e);
|
|
1162
|
-
return
|
|
1214
|
+
return { ok: false };
|
|
1163
1215
|
}
|
|
1164
1216
|
}
|
|
1165
1217
|
/** Remove the stored custom font (server deletes every variant + clears the
|
|
@@ -1340,6 +1392,12 @@ window.__ModuleLoader__.load({
|
|
|
1340
1392
|
v
|
|
1341
1393
|
];
|
|
1342
1394
|
}
|
|
1395
|
+
/** Format an RGB triple as a lowercase hex string. Channels are rounded: some
|
|
1396
|
+
* callers feed float channels and a fractional byte would produce invalid
|
|
1397
|
+
* hex. */
|
|
1398
|
+
function rgbToHex(rgb) {
|
|
1399
|
+
return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
|
|
1400
|
+
}
|
|
1343
1401
|
let tokensCacheKey = "";
|
|
1344
1402
|
let tokensCache = null;
|
|
1345
1403
|
/**
|
|
@@ -2483,9 +2541,15 @@ window.__ModuleLoader__.load({
|
|
|
2483
2541
|
wpController = null;
|
|
2484
2542
|
}
|
|
2485
2543
|
/** Register a callback fired once a generated snapshot is ready (so the caller
|
|
2486
|
-
* can re-sync the settings preview / store).
|
|
2544
|
+
* can re-sync the settings preview / store). Returns an unsubscribe: HMR
|
|
2545
|
+
* re-runs apply and would otherwise leave the previous apply's closure as the
|
|
2546
|
+
* live listener (a stale this-session callback invoked by a lingering
|
|
2547
|
+
* controller). */
|
|
2487
2548
|
function onGeneratedSnapshot(cb) {
|
|
2488
2549
|
snapshotListener = cb;
|
|
2550
|
+
return () => {
|
|
2551
|
+
if (snapshotListener === cb) snapshotListener = null;
|
|
2552
|
+
};
|
|
2489
2553
|
}
|
|
2490
2554
|
function ensureTokenStyle() {
|
|
2491
2555
|
if (tokenStyleEl?.isConnected) return tokenStyleEl;
|
|
@@ -2724,6 +2788,18 @@ window.__ModuleLoader__.load({
|
|
|
2724
2788
|
}
|
|
2725
2789
|
/** Write every group's stroke width/color variables + the two column strokes.
|
|
2726
2790
|
* Called from applyWp so palette/verdict changes re-derive 'auto'/'theme'. */
|
|
2791
|
+
/** Write (or clear) one column's inline text stroke. The stroke rides inline
|
|
2792
|
+
* styles and is fully removed at width 0 so nothing lingers after the slider
|
|
2793
|
+
* resets. */
|
|
2794
|
+
function applyInlineStroke(el, s) {
|
|
2795
|
+
if (s.width > 0) {
|
|
2796
|
+
el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
|
|
2797
|
+
el.style.setProperty("paint-order", "stroke fill");
|
|
2798
|
+
} else {
|
|
2799
|
+
el.style.removeProperty("-webkit-text-stroke");
|
|
2800
|
+
el.style.removeProperty("paint-order");
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2727
2803
|
function applyStrokes() {
|
|
2728
2804
|
const strokes = rStrokes();
|
|
2729
2805
|
const root = document.documentElement;
|
|
@@ -2735,15 +2811,7 @@ window.__ModuleLoader__.load({
|
|
|
2735
2811
|
discoverParts();
|
|
2736
2812
|
for (const g of STROKE_INLINE_GROUPS) {
|
|
2737
2813
|
const el = g === "bg" ? centerEl : sidebarEl;
|
|
2738
|
-
if (el
|
|
2739
|
-
const s = strokes[g];
|
|
2740
|
-
if (s.width > 0) {
|
|
2741
|
-
el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
|
|
2742
|
-
el.style.setProperty("paint-order", "stroke fill");
|
|
2743
|
-
} else {
|
|
2744
|
-
el.style.removeProperty("-webkit-text-stroke");
|
|
2745
|
-
el.style.removeProperty("paint-order");
|
|
2746
|
-
}
|
|
2814
|
+
if (el !== null) applyInlineStroke(el, strokes[g]);
|
|
2747
2815
|
}
|
|
2748
2816
|
}
|
|
2749
2817
|
/** Live per-group stroke update during slider drag (no full re-apply). */
|
|
@@ -2756,13 +2824,7 @@ window.__ModuleLoader__.load({
|
|
|
2756
2824
|
discoverParts();
|
|
2757
2825
|
const el = part === "bg" ? centerEl : sidebarEl;
|
|
2758
2826
|
if (el === null) return;
|
|
2759
|
-
|
|
2760
|
-
el.style.setProperty("-webkit-text-stroke", `${s.width}px ${strokeColor(s)}`);
|
|
2761
|
-
el.style.setProperty("paint-order", "stroke fill");
|
|
2762
|
-
} else {
|
|
2763
|
-
el.style.removeProperty("-webkit-text-stroke");
|
|
2764
|
-
el.style.removeProperty("paint-order");
|
|
2765
|
-
}
|
|
2827
|
+
applyInlineStroke(el, s);
|
|
2766
2828
|
}
|
|
2767
2829
|
/** Teardown only: drop every stroke variable and column inline stroke. */
|
|
2768
2830
|
function removeStrokes() {
|
|
@@ -2867,27 +2929,35 @@ window.__ModuleLoader__.load({
|
|
|
2867
2929
|
if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-panel", `blur(${px}px)`);
|
|
2868
2930
|
else document.documentElement.style.removeProperty("--dsh-any-blur-panel");
|
|
2869
2931
|
}
|
|
2870
|
-
|
|
2932
|
+
/** Re-scope one surface group's layer tokens onto plugin-owned variables so a
|
|
2933
|
+
* dedicated slider owns its alpha (the host has no per-surface opacity). The
|
|
2934
|
+
* settings group additionally retints its dialog surface variable. Always
|
|
2935
|
+
* written explicitly — including at 100% — so removing a slider's effect means
|
|
2936
|
+
* writing 1, not deleting the variable (the style rules below have no fallback
|
|
2937
|
+
* and would otherwise resolve the body tokens that applyCustomTokens rewrote
|
|
2938
|
+
* with the homepage card alpha). */
|
|
2939
|
+
function applyLayerOverrides(prefix, op, surface) {
|
|
2871
2940
|
const [h, s, l] = rColor();
|
|
2872
2941
|
const tokens = genTokens(h, s, l, rColorScheme()).tokens;
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2942
|
+
if (surface) {
|
|
2943
|
+
const surfaceColor = tokens["--dsw-alias-bg-layer-2"];
|
|
2944
|
+
if (surfaceColor !== void 0) document.documentElement.style.setProperty(`${prefix}-surface`, toRgba(surfaceColor, op));
|
|
2945
|
+
}
|
|
2946
|
+
for (const layer of [
|
|
2947
|
+
1,
|
|
2948
|
+
2,
|
|
2949
|
+
3
|
|
2950
|
+
]) {
|
|
2951
|
+
const c = tokens[`--dsw-alias-bg-layer-${layer}`];
|
|
2952
|
+
if (c !== void 0) document.documentElement.style.setProperty(`${prefix}-layer-${layer}`, toRgba(c, op));
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
function applySettingsOverrides(op) {
|
|
2956
|
+
applyLayerOverrides("--dsh-any-bg-settings", op, true);
|
|
2880
2957
|
}
|
|
2881
2958
|
const TRAJECTORY_STYLE_RULE = "[data-conversation-composer-overlay]{--dsw-alias-bg-layer-1:var(--dsh-any-traj-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-traj-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-traj-layer-3)}";
|
|
2882
2959
|
function applyTrajectoryOverrides(op) {
|
|
2883
|
-
|
|
2884
|
-
const tokens = genTokens(h, s, l, rColorScheme()).tokens;
|
|
2885
|
-
const layer1 = tokens["--dsw-alias-bg-layer-1"];
|
|
2886
|
-
const layer2 = tokens["--dsw-alias-bg-layer-2"];
|
|
2887
|
-
const layer3 = tokens["--dsw-alias-bg-layer-3"];
|
|
2888
|
-
if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-1", toRgba(layer1, op));
|
|
2889
|
-
if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-2", toRgba(layer2, op));
|
|
2890
|
-
if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-3", toRgba(layer3, op));
|
|
2960
|
+
applyLayerOverrides("--dsh-any-traj", op, false);
|
|
2891
2961
|
}
|
|
2892
2962
|
/** Register a callback fired when a wallpaper-extracted theme color is
|
|
2893
2963
|
* adopted by the auto-adaptation path, so the section can re-register the
|
|
@@ -2896,6 +2966,9 @@ window.__ModuleLoader__.load({
|
|
|
2896
2966
|
let colorAdoptedListener = null;
|
|
2897
2967
|
function onColorAdopted(cb) {
|
|
2898
2968
|
colorAdoptedListener = cb;
|
|
2969
|
+
return () => {
|
|
2970
|
+
if (colorAdoptedListener === cb) colorAdoptedListener = null;
|
|
2971
|
+
};
|
|
2899
2972
|
}
|
|
2900
2973
|
/** Apply the theme color: use the saved pick directly, or fall back to
|
|
2901
2974
|
* extracting a dominant color from the current wallpaper. */
|
|
@@ -2911,8 +2984,8 @@ window.__ModuleLoader__.load({
|
|
|
2911
2984
|
if (hsl && rWp() === url) {
|
|
2912
2985
|
cfg.color = hsl;
|
|
2913
2986
|
colorAdoptedListener?.(hsl);
|
|
2987
|
+
applyWp();
|
|
2914
2988
|
}
|
|
2915
|
-
applyWp();
|
|
2916
2989
|
});
|
|
2917
2990
|
} else applyWp();
|
|
2918
2991
|
}
|
|
@@ -2944,7 +3017,7 @@ window.__ModuleLoader__.load({
|
|
|
2944
3017
|
* preserving the user's scale/intensity/speed/density/preset choices. */
|
|
2945
3018
|
function regenerateGeneratedBg() {
|
|
2946
3019
|
const params = cfg.generatedBg;
|
|
2947
|
-
if (!params || cfg.backgroundType === "image") return;
|
|
3020
|
+
if (!params || cfg.backgroundType === "image" || cfg.backgroundType === "video") return;
|
|
2948
3021
|
cfg.generatedBg = {
|
|
2949
3022
|
...params,
|
|
2950
3023
|
seed: randomSeed()
|
|
@@ -2967,9 +3040,10 @@ window.__ModuleLoader__.load({
|
|
|
2967
3040
|
wpEl.appendChild(wpController.canvas);
|
|
2968
3041
|
}
|
|
2969
3042
|
requestAnimationFrame(() => {
|
|
2970
|
-
if (!wpController) return;
|
|
2971
3043
|
const controller = wpController;
|
|
3044
|
+
if (controller === null) return;
|
|
2972
3045
|
const frame = controller.snapshot();
|
|
3046
|
+
if (wpController !== controller) return;
|
|
2973
3047
|
setWpUrl(frame);
|
|
2974
3048
|
applyWp();
|
|
2975
3049
|
snapshotListener?.();
|
|
@@ -2996,35 +3070,36 @@ window.__ModuleLoader__.load({
|
|
|
2996
3070
|
partBlurStyleEl.textContent = PART_BLUR_RULE;
|
|
2997
3071
|
document.head.appendChild(partBlurStyleEl);
|
|
2998
3072
|
}
|
|
2999
|
-
/** Find one direct child of `frame` whose `[data-rightbar-col]` marker matches.
|
|
3000
|
-
* The rightbar is its own grid track, NOT a "details" twin of the center
|
|
3001
|
-
* column, so it must never receive the main-bg tint or blur underlay. */
|
|
3002
|
-
function findRightbarInFrame(frame) {
|
|
3003
|
-
for (let i = 0; i < frame.children.length; i++) {
|
|
3004
|
-
const child = frame.children[i];
|
|
3005
|
-
if (child instanceof HTMLElement && child.dataset.rightbarCol !== void 0) return child;
|
|
3006
|
-
}
|
|
3007
|
-
return null;
|
|
3008
|
-
}
|
|
3009
3073
|
function discoverParts() {
|
|
3010
3074
|
const overlay = document.querySelector("[data-shell-overlay]");
|
|
3011
3075
|
if (overlay === null) return;
|
|
3012
3076
|
const frame = overlay.parentElement;
|
|
3013
3077
|
if (frame === null) return;
|
|
3014
3078
|
frameEl = frame;
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3079
|
+
let overlayIdx = -1;
|
|
3080
|
+
let rightIdx = -1;
|
|
3081
|
+
let right = null;
|
|
3082
|
+
const children = frame.children;
|
|
3083
|
+
for (let i = 0; i < children.length; i++) {
|
|
3084
|
+
const child = children[i];
|
|
3085
|
+
if (child === overlay) overlayIdx = i;
|
|
3086
|
+
else if (child instanceof HTMLElement && child.dataset.rightbarCol !== void 0) {
|
|
3087
|
+
right = child;
|
|
3088
|
+
rightIdx = i;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
rightEl = right;
|
|
3092
|
+
if (right !== null) {
|
|
3093
|
+
sidebarEl = children[rightIdx - 2] ?? null;
|
|
3094
|
+
centerEl = children[rightIdx - 1] ?? null;
|
|
3021
3095
|
} else {
|
|
3022
|
-
sidebarEl =
|
|
3023
|
-
centerEl =
|
|
3096
|
+
sidebarEl = children[overlayIdx - 3] ?? null;
|
|
3097
|
+
centerEl = children[overlayIdx - 2] ?? null;
|
|
3024
3098
|
}
|
|
3025
3099
|
}
|
|
3026
3100
|
function setBlur(el, px) {
|
|
3027
3101
|
if (el === null) return;
|
|
3102
|
+
if (px === 0 && !el.classList.contains(PART_BLUR_CLASS) && el.getAttribute("data-dab-pos-patched") !== "1") return;
|
|
3028
3103
|
const underlay = el.querySelector(`:scope > .${PART_UNDERLAY_CLASS}`);
|
|
3029
3104
|
if (px > 0) {
|
|
3030
3105
|
ensurePartBlurStyle();
|
|
@@ -3158,6 +3233,34 @@ window.__ModuleLoader__.load({
|
|
|
3158
3233
|
plain: true
|
|
3159
3234
|
}];
|
|
3160
3235
|
const viewTargets = VIEW_CARDS.map(() => null);
|
|
3236
|
+
/** View specs whose real host marker has been seen at least once. Until a
|
|
3237
|
+
* marker is seen, its absence is ambiguous — "that view is not mounted" or
|
|
3238
|
+
* "this build has no such marker, keep running the generic fallback" — and the
|
|
3239
|
+
* pass has to keep probing. Once seen, absence can only mean the former, so the
|
|
3240
|
+
* pass may be skipped.
|
|
3241
|
+
*
|
|
3242
|
+
* This matters because the two specs are two different MAIN VIEWS and only one
|
|
3243
|
+
* of them is mounted at a time (the host renders the main slot for the active
|
|
3244
|
+
* panel id, keyed by `entryKey` — see AppFrame's MainPanel). So "every surface
|
|
3245
|
+
* present" is never true during a normal session, and an all-or-nothing gate on
|
|
3246
|
+
* it silently never engages: that is exactly how the previous version of this
|
|
3247
|
+
* observer ended up running the full pass on every animation frame while
|
|
3248
|
+
* tokens streamed. */
|
|
3249
|
+
const markerSeen = /* @__PURE__ */ new Set();
|
|
3250
|
+
/** Resolve just the stable host markers for the view specs — one querySelector
|
|
3251
|
+
* each, no fallback heuristics. Cheap enough to run on every coalesced burst,
|
|
3252
|
+
* and enough to notice a view mounting/unmounting. */
|
|
3253
|
+
function probeViewMarkers() {
|
|
3254
|
+
const center = centerEl;
|
|
3255
|
+
if (center === null || !document.body.contains(center)) return VIEW_CARDS.map(() => null);
|
|
3256
|
+
return VIEW_CARDS.map((spec) => center.querySelector(spec.sel));
|
|
3257
|
+
}
|
|
3258
|
+
/** Whether the pass must run regardless of the value/identity signature. */
|
|
3259
|
+
function needsPartsPass(markers) {
|
|
3260
|
+
if (!columnsPresent()) return true;
|
|
3261
|
+
if (markers.some((el, i) => el !== null && el !== viewTargets[i])) return true;
|
|
3262
|
+
return VIEW_CARDS.some((spec, i) => spec.fallback === true && markers[i] === null && !markerSeen.has(i));
|
|
3263
|
+
}
|
|
3161
3264
|
function isScrollableY(el) {
|
|
3162
3265
|
const oy = getComputedStyle(el).overflowY;
|
|
3163
3266
|
return oy === "auto" || oy === "scroll" || oy === "overlay";
|
|
@@ -3202,6 +3305,7 @@ window.__ModuleLoader__.load({
|
|
|
3202
3305
|
const marked = centerEl.querySelector(spec.sel);
|
|
3203
3306
|
const cached = viewTargets[idx];
|
|
3204
3307
|
if (marked !== null) {
|
|
3308
|
+
markerSeen.add(idx);
|
|
3205
3309
|
if (cached !== null && cached !== marked) {
|
|
3206
3310
|
setBlur(cached, 0);
|
|
3207
3311
|
restoreCardHost(cached, spec.mark, spec.prev, spec.plain === true);
|
|
@@ -3337,15 +3441,48 @@ window.__ModuleLoader__.load({
|
|
|
3337
3441
|
syncTableFix();
|
|
3338
3442
|
}
|
|
3339
3443
|
let partsObserver = null;
|
|
3444
|
+
let partsApplyRaf = 0;
|
|
3445
|
+
/** Signature of the last blur+opacity pass we actually wrote to the DOM. */
|
|
3446
|
+
let appliedPartsKey = "";
|
|
3447
|
+
/** The surfaces the last pass styled, compared BY IDENTITY: the host can swap
|
|
3448
|
+
* a view element for a fresh one while the slider values stay identical, and
|
|
3449
|
+
* the value signature alone would then skip the pass and leave the new view
|
|
3450
|
+
* unstyled until a slider moves. */
|
|
3451
|
+
let appliedTargets = [];
|
|
3452
|
+
/** The four frame columns are mounted and still attached. These are the frame's
|
|
3453
|
+
* own children (see the host's AppFrame: sidebar / center / rightbar carrying
|
|
3454
|
+
* `data-rightbar-col` / the overlay), so they exist as soon as the shell does —
|
|
3455
|
+
* unlike the per-view cards below, which come and go with the active view. */
|
|
3456
|
+
function columnsPresent() {
|
|
3457
|
+
return frameEl !== null && sidebarEl !== null && centerEl !== null && rightEl !== null && document.body.contains(frameEl) && document.body.contains(centerEl);
|
|
3458
|
+
}
|
|
3459
|
+
/** Snapshot of every surface the pass would style, for the identity check. */
|
|
3460
|
+
function targetsNow() {
|
|
3461
|
+
return [
|
|
3462
|
+
frameEl,
|
|
3463
|
+
sidebarEl,
|
|
3464
|
+
centerEl,
|
|
3465
|
+
rightEl,
|
|
3466
|
+
...viewTargets
|
|
3467
|
+
];
|
|
3468
|
+
}
|
|
3340
3469
|
/** Watch for the AppFrame mounting so persisted blurs land even when the shell
|
|
3341
|
-
* renders after this plugin's apply.
|
|
3342
|
-
* callback returns. */
|
|
3470
|
+
* renders after this plugin's apply. */
|
|
3343
3471
|
function watchParts() {
|
|
3344
3472
|
if (partsObserver !== null || typeof MutationObserver === "undefined") return;
|
|
3345
3473
|
partsObserver = new MutationObserver(() => {
|
|
3346
|
-
if (
|
|
3347
|
-
|
|
3348
|
-
|
|
3474
|
+
if (partsApplyRaf !== 0) return;
|
|
3475
|
+
partsApplyRaf = requestAnimationFrame(() => {
|
|
3476
|
+
partsApplyRaf = 0;
|
|
3477
|
+
const blurs = rBlurs();
|
|
3478
|
+
const ops = rOps();
|
|
3479
|
+
const key = JSON.stringify([blurs, ops]);
|
|
3480
|
+
if (!needsPartsPass(probeViewMarkers()) && key === appliedPartsKey && !targetsNow().some((el, i) => el !== appliedTargets[i])) return;
|
|
3481
|
+
applyPartBlurs(blurs);
|
|
3482
|
+
applyPartOpacities(ops);
|
|
3483
|
+
appliedPartsKey = key;
|
|
3484
|
+
appliedTargets = targetsNow();
|
|
3485
|
+
});
|
|
3349
3486
|
});
|
|
3350
3487
|
partsObserver.observe(document.body, {
|
|
3351
3488
|
childList: true,
|
|
@@ -3355,6 +3492,13 @@ window.__ModuleLoader__.load({
|
|
|
3355
3492
|
function stopWatchingParts() {
|
|
3356
3493
|
partsObserver?.disconnect();
|
|
3357
3494
|
partsObserver = null;
|
|
3495
|
+
if (partsApplyRaf !== 0) {
|
|
3496
|
+
cancelAnimationFrame(partsApplyRaf);
|
|
3497
|
+
partsApplyRaf = 0;
|
|
3498
|
+
}
|
|
3499
|
+
appliedPartsKey = "";
|
|
3500
|
+
appliedTargets = [];
|
|
3501
|
+
markerSeen.clear();
|
|
3358
3502
|
}
|
|
3359
3503
|
let themeObserver = null;
|
|
3360
3504
|
let themeRaf = 0;
|
|
@@ -3507,6 +3651,9 @@ window.__ModuleLoader__.load({
|
|
|
3507
3651
|
* be re-registered through the host theme service. */
|
|
3508
3652
|
function onVerdictApplied(cb) {
|
|
3509
3653
|
verdictListener = cb;
|
|
3654
|
+
return () => {
|
|
3655
|
+
if (verdictListener === cb) verdictListener = null;
|
|
3656
|
+
};
|
|
3510
3657
|
}
|
|
3511
3658
|
function applyVerdict(dark) {
|
|
3512
3659
|
if (rBgDark() === dark) return;
|
|
@@ -3634,6 +3781,8 @@ window.__ModuleLoader__.load({
|
|
|
3634
3781
|
clearVideoEl();
|
|
3635
3782
|
wpEl?.remove();
|
|
3636
3783
|
wpEl = null;
|
|
3784
|
+
wpVerdict = null;
|
|
3785
|
+
setBgDark(null);
|
|
3637
3786
|
}
|
|
3638
3787
|
} else if (cfg.backgroundType !== "image" && cfg.generatedBg) {
|
|
3639
3788
|
clearVideoEl();
|
|
@@ -3705,8 +3854,14 @@ window.__ModuleLoader__.load({
|
|
|
3705
3854
|
pendingOps = null;
|
|
3706
3855
|
tableFixStyleEl?.remove();
|
|
3707
3856
|
tableFixStyleEl = null;
|
|
3857
|
+
partBlurStyleEl?.remove();
|
|
3858
|
+
partBlurStyleEl = null;
|
|
3708
3859
|
removeStrokes();
|
|
3709
3860
|
removeFontFace();
|
|
3861
|
+
imgNat = null;
|
|
3862
|
+
lowResUrl = null;
|
|
3863
|
+
lowResFor = "";
|
|
3864
|
+
dragLow = false;
|
|
3710
3865
|
setBlur(frameEl, 0);
|
|
3711
3866
|
setBlur(sidebarEl, 0);
|
|
3712
3867
|
setBlur(centerEl, 0);
|
|
@@ -3842,7 +3997,10 @@ window.__ModuleLoader__.load({
|
|
|
3842
3997
|
/* ── wheel card ──────────────────────────────────────────────────────────── */
|
|
3843
3998
|
.dab-wheel-card{position:relative;display:flex;align-items:center;justify-content:center;gap:28px;flex-wrap:wrap;padding:24px 18px}
|
|
3844
3999
|
.dab-wheel-glow{position:absolute;width:230px;height:230px;border-radius:50%;filter:blur(48px);opacity:.2;background:var(--c,#888);pointer-events:none;transition:background .4s ease}
|
|
3845
|
-
|
|
4000
|
+
/* flex:none is load bearing, not cosmetic: the pointer maths reads client
|
|
4001
|
+
* coordinates straight off this element's box, so any shrink below the 220px
|
|
4002
|
+
* drawing surface would desynchronise the ring from the cursor. */
|
|
4003
|
+
.dab-wheel{position:relative;flex:none;cursor:crosshair;border-radius:50%;box-shadow:0 12px 32px -14px rgba(0,0,0,.4)}
|
|
3846
4004
|
.dab-hint{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);padding:0 4px}
|
|
3847
4005
|
|
|
3848
4006
|
/* ── precise color inputs ────────────────────────────────────────────────── */
|
|
@@ -3896,7 +4054,7 @@ window.__ModuleLoader__.load({
|
|
|
3896
4054
|
.dab-slider::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:var(--dsw-alias-button-elevated-fill,#fff);border:2px solid var(--dsw-alias-brand-primary)}
|
|
3897
4055
|
|
|
3898
4056
|
/* ── interface part cards ────────────────────────────────────────────────── */
|
|
3899
|
-
.dab-grid-parts{display:grid;grid-template-columns:repeat(auto-fill,minmax(
|
|
4057
|
+
.dab-grid-parts{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(240px,100%),1fr));gap:13px}
|
|
3900
4058
|
.dab-part-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}
|
|
3901
4059
|
.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)}
|
|
3902
4060
|
.dab-part-name{font-size:13.5px;font-weight:600}
|
|
@@ -3973,7 +4131,7 @@ window.__ModuleLoader__.load({
|
|
|
3973
4131
|
.dab-spin{animation:dab-rotate .7s cubic-bezier(.3,.7,.3,1) 1}
|
|
3974
4132
|
|
|
3975
4133
|
/* ── profile page ────────────────────────────────────────────────────────── */
|
|
3976
|
-
.dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(
|
|
4134
|
+
.dab-profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:13px}
|
|
3977
4135
|
.dab-profile-ico{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;margin-bottom:13px;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)}
|
|
3978
4136
|
.dab-profile-title{font-size:14px;font-weight:650}
|
|
3979
4137
|
.dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
|
|
@@ -3982,7 +4140,7 @@ window.__ModuleLoader__.load({
|
|
|
3982
4140
|
|
|
3983
4141
|
/* ── presets / profiles / schedule / rotation ────────────────────────────── */
|
|
3984
4142
|
.dab-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
3985
|
-
.dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
|
|
4143
|
+
.dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(150px,100%),1fr));gap:10px}
|
|
3986
4144
|
.dab-preset{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:12px;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:13px;cursor:pointer;font:inherit;text-align:left;transition:border-color .25s,transform .25s,box-shadow .25s;animation:dab-row-in .34s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--i,0) * 45ms)}
|
|
3987
4145
|
.dab-preset:hover{transform:translateY(-2px);border-color:var(--dsw-alias-brand-primary);box-shadow:0 8px 22px -12px rgba(0,0,0,.3)}
|
|
3988
4146
|
.dab-preset-dot{width:22px;height:22px;border-radius:50%;margin-bottom:4px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.12),0 4px 10px -4px rgba(0,0,0,.35);background:var(--dsw-alias-bg-layer-2);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary)}
|
|
@@ -4009,7 +4167,7 @@ window.__ModuleLoader__.load({
|
|
|
4009
4167
|
.dab-schedule-wrap.is-open{grid-template-rows:1fr;visibility:visible;transition:grid-template-rows .42s cubic-bezier(.22,1,.36,1)}
|
|
4010
4168
|
.dab-schedule-clip{overflow:hidden;min-height:0;opacity:0;transition:opacity .28s ease}
|
|
4011
4169
|
.dab-schedule-wrap.is-open .dab-schedule-clip{opacity:1;transition:opacity .32s ease .1s}
|
|
4012
|
-
.dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;padding-top:12px}
|
|
4170
|
+
.dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:16px;padding-top:12px}
|
|
4013
4171
|
.dab-schedule-cell{min-width:0;animation:dab-row-in .34s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--i,0) * 60ms)}
|
|
4014
4172
|
.dab-time-row{display:flex;flex-direction:column;gap:9px}
|
|
4015
4173
|
.dab-time-label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
|
|
@@ -4045,6 +4203,9 @@ window.__ModuleLoader__.load({
|
|
|
4045
4203
|
.dab-crash{display:flex;flex-direction:column;gap:10px;align-items:center;padding:28px 18px;border:1px solid var(--dsw-alias-border-l2);border-radius:16px}
|
|
4046
4204
|
.dab-crash-title{font-size:15px;font-weight:650}
|
|
4047
4205
|
.dab-crash-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center;line-height:1.5}
|
|
4206
|
+
/* The crash cause: readable, scrollable, and clipped to a few lines so a long
|
|
4207
|
+
* stack cannot push the reset button off the panel. */
|
|
4208
|
+
.dab-crash-detail{margin:0;max-width:min(520px,80vw);max-height:132px;overflow:auto;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);font-family:var(--dab-mono);font-size:10.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));text-align:left;white-space:pre-wrap;word-break:break-word}
|
|
4048
4209
|
|
|
4049
4210
|
/* ── keyframes ───────────────────────────────────────────────────────────── */
|
|
4050
4211
|
@keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
|
|
@@ -4060,6 +4221,72 @@ window.__ModuleLoader__.load({
|
|
|
4060
4221
|
@keyframes dab-zoom-in{from{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:none}}
|
|
4061
4222
|
@keyframes dab-flow{to{background-position:300% 50%}}
|
|
4062
4223
|
|
|
4224
|
+
/* ── sidebar surface (a dsh-better-sidebar page) ─────────────────────────────
|
|
4225
|
+
* Same shell, hosted in a panel that is handed a full-height column (the host's
|
|
4226
|
+
* native tab body is height:100%) and whose width the user drags freely. So only
|
|
4227
|
+
* THREE things are fixed here, all of them structural:
|
|
4228
|
+
* 1. the shell fills the height and the page body owns the scroll
|
|
4229
|
+
* (flex:1 + min-height:0), so the panel stays one scrollable column instead
|
|
4230
|
+
* of pushing the whole tab into a scrollbar;
|
|
4231
|
+
* 2. the nav is a rail ABOVE the page rather than beside it — a side rail would
|
|
4232
|
+
* spend 150px of a panel that is narrow by nature, and this way the page
|
|
4233
|
+
* keeps the full width for its own grids;
|
|
4234
|
+
* 3. long tokens (paths, hex, URLs) may wrap instead of widening the panel.
|
|
4235
|
+
*
|
|
4236
|
+
* Everything else — grid columns, paddings, type scale, whether the cards lift on
|
|
4237
|
+
* hover — is width-driven through the container queries below, which measure this
|
|
4238
|
+
* shell (the .dab-root container, which declares container-type:inline-size) and
|
|
4239
|
+
* not the viewport. A narrow sidebar compacts; a wide one falls back to the
|
|
4240
|
+
* dialog's own spacing and multi-column grids, because at that point it *is* a
|
|
4241
|
+
* dialog-sized surface. */
|
|
4242
|
+
[data-dab-surface="sidebar"].dab-root{height:100%;min-height:0;padding:12px 12px 0}
|
|
4243
|
+
[data-dab-surface="sidebar"] .dab-shell{display:flex;flex-direction:column;align-items:stretch;gap:11px;height:100%;min-height:0;max-width:none;padding-bottom:0}
|
|
4244
|
+
[data-dab-surface="sidebar"] .dab-nav{gap:0}
|
|
4245
|
+
[data-dab-surface="sidebar"] .dab-nav-list{flex-direction:row;gap:2px;overflow-x:auto;overflow-y:hidden;padding-bottom:1px}
|
|
4246
|
+
[data-dab-surface="sidebar"] .dab-nav-list::-webkit-scrollbar{display:none}
|
|
4247
|
+
[data-dab-surface="sidebar"] .dab-nav-ind{display:none}
|
|
4248
|
+
[data-dab-surface="sidebar"] .dab-nav-item{flex:none;height:32px;gap:7px;padding:0 10px;font-size:12px}
|
|
4249
|
+
[data-dab-surface="sidebar"] .dab-nav-item.is-active{background:var(--dsw-alias-bg-layer-2)}
|
|
4250
|
+
[data-dab-surface="sidebar"] .dab-page{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding-bottom:16px;scrollbar-width:thin}
|
|
4251
|
+
/* Last-resort safety for text that cannot wrap (paths, hex, URLs). */
|
|
4252
|
+
[data-dab-surface="sidebar"] .dab-hint,[data-dab-surface="sidebar"] .dab-part-name{overflow-wrap:anywhere}
|
|
4253
|
+
/* The panel's width is whatever the user dragged it to, so let the part cards
|
|
4254
|
+
* fill as many columns as actually fit instead of hard-coding one. 170px is the
|
|
4255
|
+
* narrowest a part card stays readable once its header can wrap (see the head
|
|
4256
|
+
* rules below), which puts the second column at roughly a 380px panel — the
|
|
4257
|
+
* default right-sidebar width. */
|
|
4258
|
+
[data-dab-surface="sidebar"] .dab-grid-parts{grid-template-columns:repeat(auto-fill,minmax(min(170px,100%),1fr));gap:12px}
|
|
4259
|
+
[data-dab-surface="sidebar"] .dab-types{grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr))}
|
|
4260
|
+
[data-dab-surface="sidebar"] .dab-card{padding:12px}
|
|
4261
|
+
[data-dab-surface="sidebar"] .dab-part-head{gap:8px;margin-bottom:10px}
|
|
4262
|
+
[data-dab-surface="sidebar"] .dab-part-ico{width:26px;height:26px;border-radius:8px}
|
|
4263
|
+
[data-dab-surface="sidebar"] .dab-part-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px}
|
|
4264
|
+
[data-dab-surface="sidebar"] .dab-part-badge{padding:2px 7px;font-size:10px}
|
|
4265
|
+
|
|
4266
|
+
/* Narrow panel: tighten the chrome so the content still has room. */
|
|
4267
|
+
@container (max-width:539px){
|
|
4268
|
+
[data-dab-surface="sidebar"] .dab-page{gap:11px}
|
|
4269
|
+
[data-dab-surface="sidebar"] .dab-h1{font-size:17px}
|
|
4270
|
+
[data-dab-surface="sidebar"] .dab-desc{font-size:12px}
|
|
4271
|
+
[data-dab-surface="sidebar"] .dab-orb-wrap{width:96px;height:96px}
|
|
4272
|
+
[data-dab-surface="sidebar"] .dab-wheel-card{padding:14px 10px;gap:16px}
|
|
4273
|
+
[data-dab-surface="sidebar"] .dab-wheel-glow{width:170px;height:170px}
|
|
4274
|
+
/* Lift-on-hover is a dialog affordance; in a cramped scrolling panel it only
|
|
4275
|
+
* adds jitter under the cursor. */
|
|
4276
|
+
[data-dab-surface="sidebar"] .dab-card-hover:hover{transform:none;box-shadow:none}
|
|
4277
|
+
}
|
|
4278
|
+
/* Roomy panel: behave like the dialog again — its spacing, its type scale, and
|
|
4279
|
+
* multi-column grids (a smaller track minimum than the dialog, because the page
|
|
4280
|
+
* here is a few hundred px narrower than a dialog's content column). */
|
|
4281
|
+
@container (min-width:540px){
|
|
4282
|
+
[data-dab-surface="sidebar"] .dab-shell{gap:16px}
|
|
4283
|
+
[data-dab-surface="sidebar"] .dab-page{gap:13px}
|
|
4284
|
+
[data-dab-surface="sidebar"] .dab-nav-item{height:34px;font-size:12.5px;padding:0 12px}
|
|
4285
|
+
/* The shared ≤620px rule flattens the type cards to one column, which is right
|
|
4286
|
+
* for a narrow phone-width dialog but wasteful in a roomy panel. */
|
|
4287
|
+
[data-dab-surface="sidebar"] .dab-types{grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4063
4290
|
/* ── responsive & motion preferences ─────────────────────────────────────── */
|
|
4064
4291
|
@container (max-width:620px){
|
|
4065
4292
|
.dab-shell{grid-template-columns:1fr;gap:14px}
|
|
@@ -4074,6 +4301,30 @@ window.__ModuleLoader__.load({
|
|
|
4074
4301
|
@container (min-width:621px){
|
|
4075
4302
|
.dab-shell{grid-template-columns:158px minmax(0,1fr);gap:26px}
|
|
4076
4303
|
}
|
|
4304
|
+
/* Phone: on a handset the settings dialog's content column shrinks to roughly
|
|
4305
|
+
* the viewport, and the dialog-sized chrome (18px cards, 21px headings, a 118px
|
|
4306
|
+
* orb, 24px paddings) stops fitting — cards end up wider than the column or the
|
|
4307
|
+
* color row wraps into three cramped pieces. These are the same tightenings the
|
|
4308
|
+
* sidebar surface applies at its own narrow widths, so a phone settings dialog
|
|
4309
|
+
* and a narrow sidebar panel end up looking the same. */
|
|
4310
|
+
@container (max-width:480px){
|
|
4311
|
+
.dab-shell{gap:10px}
|
|
4312
|
+
.dab-page{gap:10px}
|
|
4313
|
+
.dab-head{margin:0 0 2px}
|
|
4314
|
+
.dab-h1{font-size:18px}
|
|
4315
|
+
.dab-desc{font-size:12px}
|
|
4316
|
+
.dab-card{padding:14px;border-radius:14px}
|
|
4317
|
+
.dab-hero-accent{gap:16px}
|
|
4318
|
+
.dab-orb-wrap{width:92px;height:92px}
|
|
4319
|
+
.dab-wheel-card{padding:14px 10px;gap:16px}
|
|
4320
|
+
.dab-wheel-glow{width:180px;height:180px}
|
|
4321
|
+
/* Three fixed type-card columns need ~400px; two adaptive ones fit 320px. */
|
|
4322
|
+
.dab-types{grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr))}
|
|
4323
|
+
.dab-grid-parts{gap:11px}
|
|
4324
|
+
.dab-part-head{margin-bottom:10px}
|
|
4325
|
+
.dab-slider-block + .dab-slider-block{margin-top:10px}
|
|
4326
|
+
.dab-footer{padding-top:10px}
|
|
4327
|
+
}
|
|
4077
4328
|
@media (prefers-reduced-motion:reduce){
|
|
4078
4329
|
.dab-root *,.dab-root *::before,.dab-root *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}
|
|
4079
4330
|
}
|
|
@@ -4404,25 +4655,32 @@ window.__ModuleLoader__.load({
|
|
|
4404
4655
|
this.props.onReset?.();
|
|
4405
4656
|
};
|
|
4406
4657
|
render() {
|
|
4407
|
-
if (this.state.error)
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4658
|
+
if (this.state.error) {
|
|
4659
|
+
const detail = this.state.error.stack ?? `${this.state.error.name}: ${this.state.error.message}`;
|
|
4660
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4661
|
+
className: "dab-crash",
|
|
4662
|
+
children: [
|
|
4663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4664
|
+
className: "dab-crash-title",
|
|
4665
|
+
children: this.props.t("crashTitle")
|
|
4666
|
+
}),
|
|
4667
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4668
|
+
className: "dab-crash-desc",
|
|
4669
|
+
children: this.props.t("crashDesc")
|
|
4670
|
+
}),
|
|
4671
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
4672
|
+
className: "dab-crash-detail",
|
|
4673
|
+
children: detail
|
|
4674
|
+
}),
|
|
4675
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4676
|
+
type: "button",
|
|
4677
|
+
className: "dab-btn",
|
|
4678
|
+
onClick: this.reset,
|
|
4679
|
+
children: this.props.t("crashReset")
|
|
4680
|
+
})
|
|
4681
|
+
]
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4426
4684
|
return this.props.children;
|
|
4427
4685
|
}
|
|
4428
4686
|
};
|
|
@@ -4571,6 +4829,10 @@ window.__ModuleLoader__.load({
|
|
|
4571
4829
|
}, [col]);
|
|
4572
4830
|
const pendingRef = (0, react.useRef)(null);
|
|
4573
4831
|
const rafRef = (0, react.useRef)(null);
|
|
4832
|
+
/** Unmount-time cleanup for a drag that never saw its mouseup (panel closed
|
|
4833
|
+
* mid-drag): without it the document listeners stay attached forever and the
|
|
4834
|
+
* pending rAF keeps calling setState on a dead component. */
|
|
4835
|
+
const dragCleanupRef = (0, react.useRef)(null);
|
|
4574
4836
|
const flushPending = (0, react.useCallback)(() => {
|
|
4575
4837
|
rafRef.current = null;
|
|
4576
4838
|
const p = pendingRef.current;
|
|
@@ -4612,13 +4874,25 @@ window.__ModuleLoader__.load({
|
|
|
4612
4874
|
schedule(colRef.current.hue, s, l);
|
|
4613
4875
|
}
|
|
4614
4876
|
};
|
|
4615
|
-
const
|
|
4877
|
+
const stopDrag = () => {
|
|
4616
4878
|
document.removeEventListener("mousemove", onMove);
|
|
4617
|
-
document.removeEventListener("mouseup",
|
|
4879
|
+
document.removeEventListener("mouseup", stopDrag);
|
|
4880
|
+
dragCleanupRef.current = null;
|
|
4618
4881
|
};
|
|
4882
|
+
dragCleanupRef.current = stopDrag;
|
|
4619
4883
|
document.addEventListener("mousemove", onMove);
|
|
4620
|
-
document.addEventListener("mouseup",
|
|
4884
|
+
document.addEventListener("mouseup", stopDrag);
|
|
4621
4885
|
}, [schedule]);
|
|
4886
|
+
(0, react.useEffect)(() => {
|
|
4887
|
+
return () => {
|
|
4888
|
+
if (rafRef.current !== null) {
|
|
4889
|
+
cancelAnimationFrame(rafRef.current);
|
|
4890
|
+
rafRef.current = null;
|
|
4891
|
+
}
|
|
4892
|
+
pendingRef.current = null;
|
|
4893
|
+
dragCleanupRef.current?.();
|
|
4894
|
+
};
|
|
4895
|
+
}, []);
|
|
4622
4896
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
4623
4897
|
ref: cvsRef,
|
|
4624
4898
|
width: WHEEL_SIZE,
|
|
@@ -4667,6 +4941,7 @@ window.__ModuleLoader__.load({
|
|
|
4667
4941
|
},
|
|
4668
4942
|
onChange: (e) => {
|
|
4669
4943
|
setText(e.target.value);
|
|
4944
|
+
if (e.target.value === "") return;
|
|
4670
4945
|
const v = Number(e.target.value);
|
|
4671
4946
|
if (Number.isFinite(v)) onChange(clamp(v, min, max));
|
|
4672
4947
|
}
|
|
@@ -4783,9 +5058,6 @@ window.__ModuleLoader__.load({
|
|
|
4783
5058
|
//#region src/client/components/ColorPicker.tsx
|
|
4784
5059
|
const MAG_SIZE = 96;
|
|
4785
5060
|
const MAG_ZOOM = 8;
|
|
4786
|
-
function toHex$1(rgb) {
|
|
4787
|
-
return "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
4788
|
-
}
|
|
4789
5061
|
/**
|
|
4790
5062
|
* Eyedropper modal: shows the wallpaper full-bleed (no drag/zoom) and lets the
|
|
4791
5063
|
* user click any pixel to adopt it as the theme color. A magnifier circle
|
|
@@ -4815,7 +5087,7 @@ window.__ModuleLoader__.load({
|
|
|
4815
5087
|
canvas.height = imgRef.current.naturalHeight;
|
|
4816
5088
|
const ctx = canvas.getContext("2d");
|
|
4817
5089
|
if (ctx) ctx.drawImage(imgRef.current, 0, 0);
|
|
4818
|
-
}, [ready]);
|
|
5090
|
+
}, [ready, url]);
|
|
4819
5091
|
const sampleAt = (0, react.useCallback)((clientX, clientY) => {
|
|
4820
5092
|
const canvas = canvasRef.current;
|
|
4821
5093
|
if (!canvas) return null;
|
|
@@ -4930,14 +5202,14 @@ window.__ModuleLoader__.load({
|
|
|
4930
5202
|
height: 28,
|
|
4931
5203
|
borderRadius: 8,
|
|
4932
5204
|
border: "1px solid rgba(255,255,255,0.4)",
|
|
4933
|
-
background: hover ?
|
|
5205
|
+
background: hover ? rgbToHex(hover.rgb) : "transparent"
|
|
4934
5206
|
} }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4935
5207
|
style: {
|
|
4936
5208
|
color: "#fff",
|
|
4937
5209
|
fontSize: 13,
|
|
4938
5210
|
fontFamily: "var(--dab-mono, monospace)"
|
|
4939
5211
|
},
|
|
4940
|
-
children: hover ? `${
|
|
5212
|
+
children: hover ? `${rgbToHex(hover.rgb)} · rgb(${hover.rgb.join(", ")})` : "—"
|
|
4941
5213
|
})]
|
|
4942
5214
|
}),
|
|
4943
5215
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -5020,9 +5292,6 @@ window.__ModuleLoader__.load({
|
|
|
5020
5292
|
.58
|
|
5021
5293
|
]
|
|
5022
5294
|
];
|
|
5023
|
-
function toHex(rgb) {
|
|
5024
|
-
return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
|
|
5025
|
-
}
|
|
5026
5295
|
function ColorPage({ p, notify }) {
|
|
5027
5296
|
const { t, hue, sat, lit, setColor, extractColor, setSchemeOverride, useStore } = p;
|
|
5028
5297
|
const storeUrl = useStore((s) => s.url);
|
|
@@ -5037,7 +5306,7 @@ window.__ModuleLoader__.load({
|
|
|
5037
5306
|
];
|
|
5038
5307
|
const [h, s, l] = hsvToHsl(wheel[0], wheel[1], wheel[2]);
|
|
5039
5308
|
const [r, g, b] = hslToRgb(h, s, l);
|
|
5040
|
-
const hex =
|
|
5309
|
+
const hex = rgbToHex([
|
|
5041
5310
|
r,
|
|
5042
5311
|
g,
|
|
5043
5312
|
b
|
|
@@ -5167,7 +5436,7 @@ window.__ModuleLoader__.load({
|
|
|
5167
5436
|
type: "button",
|
|
5168
5437
|
className: `dab-swatch${on ? " is-on" : ""}`,
|
|
5169
5438
|
style: { background: `hsl(${sh} ${Math.round(ss * 100)}% ${Math.round(sl * 100)}%)` },
|
|
5170
|
-
title:
|
|
5439
|
+
title: rgbToHex(hslToRgb(sh, ss, sl)).toUpperCase(),
|
|
5171
5440
|
onClick: () => setColor(...hslToHsv(sh, ss, sl))
|
|
5172
5441
|
}, i);
|
|
5173
5442
|
})
|
|
@@ -5242,15 +5511,16 @@ window.__ModuleLoader__.load({
|
|
|
5242
5511
|
const paint = (el, v) => {
|
|
5243
5512
|
el.style.setProperty("--pct", String((v - min) / (max - min) * 100));
|
|
5244
5513
|
};
|
|
5514
|
+
const fmtRef = (0, react.useRef)(fmt);
|
|
5515
|
+
fmtRef.current = fmt;
|
|
5245
5516
|
(0, react.useEffect)(() => {
|
|
5246
5517
|
if (inputRef.current) {
|
|
5247
5518
|
inputRef.current.value = String(def);
|
|
5248
5519
|
paint(inputRef.current, def);
|
|
5249
5520
|
}
|
|
5250
|
-
if (valRef.current) valRef.current.textContent =
|
|
5521
|
+
if (valRef.current) valRef.current.textContent = fmtRef.current(def);
|
|
5251
5522
|
}, [
|
|
5252
5523
|
def,
|
|
5253
|
-
fmt,
|
|
5254
5524
|
min,
|
|
5255
5525
|
max
|
|
5256
5526
|
]);
|
|
@@ -5410,7 +5680,8 @@ window.__ModuleLoader__.load({
|
|
|
5410
5680
|
}
|
|
5411
5681
|
];
|
|
5412
5682
|
function InterfacePage({ p }) {
|
|
5413
|
-
const { t, setOps, setBlurs, setSop, setPanelOp } = p;
|
|
5683
|
+
const { t, setOps, setBlurs, setSop, setPanelOp, useStore } = p;
|
|
5684
|
+
useStore((s) => s.metaRev);
|
|
5414
5685
|
const hasBetterSidebar = useBetterSidebar();
|
|
5415
5686
|
const parts = PARTS$1.filter((part) => !(part.isPanel && !hasBetterSidebar));
|
|
5416
5687
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
@@ -5617,7 +5888,7 @@ window.__ModuleLoader__.load({
|
|
|
5617
5888
|
}
|
|
5618
5889
|
}
|
|
5619
5890
|
function FontPage({ p, notify }) {
|
|
5620
|
-
const { t, setFont, removeFont, setFontEnabled, setStrokes } = p;
|
|
5891
|
+
const { t, setFont, removeFont, setFontEnabled, setStrokes, useStore } = p;
|
|
5621
5892
|
const hasBetterSidebar = useBetterSidebar();
|
|
5622
5893
|
const fileRef = (0, react.useRef)(null);
|
|
5623
5894
|
const [strokes, setStrokesState] = (0, react.useState)(() => rStrokes());
|
|
@@ -5627,6 +5898,15 @@ window.__ModuleLoader__.load({
|
|
|
5627
5898
|
name: null
|
|
5628
5899
|
}));
|
|
5629
5900
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
5901
|
+
const metaRev = useStore((s) => s.metaRev);
|
|
5902
|
+
(0, react.useEffect)(() => {
|
|
5903
|
+
setStrokesState(rStrokes());
|
|
5904
|
+
setFontInfo((prev) => prev.mime === cfg.fontMime && prev.enabled === cfg.fontEnabled ? prev : {
|
|
5905
|
+
mime: cfg.fontMime,
|
|
5906
|
+
enabled: cfg.fontEnabled,
|
|
5907
|
+
name: null
|
|
5908
|
+
});
|
|
5909
|
+
}, [metaRev]);
|
|
5630
5910
|
const hasFont = fontInfo.mime !== null;
|
|
5631
5911
|
const fontLabel = fontInfo.name ?? (fontInfo.mime !== null ? fontInfo.mime.replace("font/", "").toUpperCase() : t("fontNone"));
|
|
5632
5912
|
/** Write one group's stroke: cfg + live DOM now, disk on commit. `live`
|
|
@@ -5651,9 +5931,9 @@ window.__ModuleLoader__.load({
|
|
|
5651
5931
|
...fontInfo,
|
|
5652
5932
|
name: file.name
|
|
5653
5933
|
});
|
|
5654
|
-
const
|
|
5934
|
+
const outcome = await setFont(file);
|
|
5655
5935
|
setBusy(false);
|
|
5656
|
-
if (ok) setFontInfo({
|
|
5936
|
+
if (outcome.ok) setFontInfo({
|
|
5657
5937
|
mime: cfg.fontMime,
|
|
5658
5938
|
enabled: cfg.fontEnabled,
|
|
5659
5939
|
name: file.name
|
|
@@ -5664,7 +5944,7 @@ window.__ModuleLoader__.load({
|
|
|
5664
5944
|
enabled: cfg.fontEnabled,
|
|
5665
5945
|
name: null
|
|
5666
5946
|
});
|
|
5667
|
-
notify(t
|
|
5947
|
+
notify(uploadRefusalText(outcome, t, "fontFail"), false);
|
|
5668
5948
|
}
|
|
5669
5949
|
};
|
|
5670
5950
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
@@ -5878,6 +6158,19 @@ window.__ModuleLoader__.load({
|
|
|
5878
6158
|
});
|
|
5879
6159
|
const containerRef = (0, react.useRef)(null);
|
|
5880
6160
|
const imgRef = (0, react.useRef)(null);
|
|
6161
|
+
/**
|
|
6162
|
+
* The overlay node is held in STATE rather than a plain ref, so the listener
|
|
6163
|
+
* effect below re-runs the moment it actually exists.
|
|
6164
|
+
*
|
|
6165
|
+
* This is not a style choice. Everything here renders through <Portal>, which
|
|
6166
|
+
* returns null on its first render — its own effect has to create the host
|
|
6167
|
+
* node under <html> first. A plain ref is therefore still null when a
|
|
6168
|
+
* mount-only effect runs, and an effect whose deps never change again attaches
|
|
6169
|
+
* nothing, ever. That is exactly how the touch handlers below ended up dead on
|
|
6170
|
+
* real phones: the wheel handler only survived because its deps churn on every
|
|
6171
|
+
* zoom/pan, so the wallpaper's img.onload re-ran it once and wired it up.
|
|
6172
|
+
*/
|
|
6173
|
+
const [overlayEl, setOverlayEl] = (0, react.useState)(null);
|
|
5881
6174
|
const dragRef = (0, react.useRef)({
|
|
5882
6175
|
active: false,
|
|
5883
6176
|
sx: 0,
|
|
@@ -5885,6 +6178,16 @@ window.__ModuleLoader__.load({
|
|
|
5885
6178
|
spx: 0,
|
|
5886
6179
|
spy: 0
|
|
5887
6180
|
});
|
|
6181
|
+
/** Unmount-time cleanup for a drag that never saw its mouseup (editor closed
|
|
6182
|
+
* mid-drag): without it the document listeners stay attached forever and
|
|
6183
|
+
* keep calling setPos on a dead component. */
|
|
6184
|
+
const dragStopRef = (0, react.useRef)(null);
|
|
6185
|
+
/** Set once a gesture actually moved, so the click that follows a drag is not
|
|
6186
|
+
* mistaken for a tap on the backdrop (mousedown and mouseup in different
|
|
6187
|
+
* subtrees make the click land on the overlay, i.e. "outside"). Reset at the
|
|
6188
|
+
* start of every gesture — touchstart / mousedown / wheel — so it can only
|
|
6189
|
+
* ever describe the gesture that just ended, never an earlier one. */
|
|
6190
|
+
const movedRef = (0, react.useRef)(false);
|
|
5888
6191
|
const viewRef = (0, react.useRef)({
|
|
5889
6192
|
zoom,
|
|
5890
6193
|
pos
|
|
@@ -5930,6 +6233,7 @@ window.__ModuleLoader__.load({
|
|
|
5930
6233
|
]);
|
|
5931
6234
|
const onDown = (0, react.useCallback)((e) => {
|
|
5932
6235
|
e.preventDefault();
|
|
6236
|
+
movedRef.current = false;
|
|
5933
6237
|
dragRef.current = {
|
|
5934
6238
|
active: true,
|
|
5935
6239
|
sx: e.clientX,
|
|
@@ -5939,58 +6243,50 @@ window.__ModuleLoader__.load({
|
|
|
5939
6243
|
};
|
|
5940
6244
|
const onMove = (ev) => {
|
|
5941
6245
|
if (!dragRef.current.active) return;
|
|
6246
|
+
if (Math.abs(ev.clientX - dragRef.current.sx) > 4 || Math.abs(ev.clientY - dragRef.current.sy) > 4) movedRef.current = true;
|
|
5942
6247
|
setPos({
|
|
5943
6248
|
x: dragRef.current.spx + ev.clientX - dragRef.current.sx,
|
|
5944
6249
|
y: dragRef.current.spy + ev.clientY - dragRef.current.sy
|
|
5945
6250
|
});
|
|
5946
6251
|
};
|
|
5947
|
-
const
|
|
6252
|
+
const stopDrag = () => {
|
|
5948
6253
|
dragRef.current.active = false;
|
|
5949
6254
|
document.removeEventListener("mousemove", onMove);
|
|
5950
|
-
document.removeEventListener("mouseup",
|
|
6255
|
+
document.removeEventListener("mouseup", stopDrag);
|
|
6256
|
+
dragStopRef.current = null;
|
|
5951
6257
|
};
|
|
6258
|
+
dragStopRef.current = stopDrag;
|
|
5952
6259
|
document.addEventListener("mousemove", onMove);
|
|
5953
|
-
document.addEventListener("mouseup",
|
|
6260
|
+
document.addEventListener("mouseup", stopDrag);
|
|
5954
6261
|
}, [pos]);
|
|
6262
|
+
(0, react.useEffect)(() => () => {
|
|
6263
|
+
dragStopRef.current?.();
|
|
6264
|
+
}, []);
|
|
5955
6265
|
const onWheelCb = (0, react.useCallback)((e) => {
|
|
5956
6266
|
e.preventDefault();
|
|
6267
|
+
movedRef.current = false;
|
|
5957
6268
|
const el = containerRef.current;
|
|
5958
6269
|
if (!el) return;
|
|
5959
6270
|
const rect = el.getBoundingClientRect();
|
|
5960
6271
|
const mx = rect.width / 2, my = rect.height / 2;
|
|
5961
6272
|
const factor = e.deltaY > 0 ? .97 : 1.03;
|
|
5962
|
-
const
|
|
5963
|
-
const
|
|
5964
|
-
const
|
|
6273
|
+
const { zoom: z, pos: p } = viewRef.current;
|
|
6274
|
+
const nz = clampZoom(z * factor);
|
|
6275
|
+
const nx = mx - (mx - p.x) * (nz / z);
|
|
6276
|
+
const ny = my - (my - p.y) * (nz / z);
|
|
5965
6277
|
setZoom(nz);
|
|
5966
6278
|
setPos({
|
|
5967
6279
|
x: nx,
|
|
5968
6280
|
y: ny
|
|
5969
6281
|
});
|
|
5970
|
-
}, [
|
|
5971
|
-
(0, react.useEffect)(() => {
|
|
5972
|
-
const el = containerRef.current;
|
|
5973
|
-
if (!el) return;
|
|
5974
|
-
el.addEventListener("wheel", onWheelCb, { passive: false });
|
|
5975
|
-
return () => el.removeEventListener("wheel", onWheelCb);
|
|
5976
|
-
}, [onWheelCb]);
|
|
6282
|
+
}, []);
|
|
5977
6283
|
const onTouchStartCb = (0, react.useCallback)((e) => {
|
|
5978
|
-
const
|
|
5979
|
-
if (!
|
|
5980
|
-
|
|
5981
|
-
pinchRef.current = null;
|
|
5982
|
-
const tp = e.touches[0];
|
|
5983
|
-
touchDragRef.current = {
|
|
5984
|
-
sx: tp.clientX,
|
|
5985
|
-
sy: tp.clientY,
|
|
5986
|
-
spx: viewRef.current.pos.x,
|
|
5987
|
-
spy: viewRef.current.pos.y
|
|
5988
|
-
};
|
|
5989
|
-
return;
|
|
5990
|
-
}
|
|
6284
|
+
const card = containerRef.current;
|
|
6285
|
+
if (!card) return;
|
|
6286
|
+
movedRef.current = false;
|
|
5991
6287
|
if (e.touches.length >= 2) {
|
|
5992
6288
|
touchDragRef.current = null;
|
|
5993
|
-
const rect =
|
|
6289
|
+
const rect = card.getBoundingClientRect();
|
|
5994
6290
|
const a = e.touches[0], b = e.touches[1];
|
|
5995
6291
|
pinchRef.current = {
|
|
5996
6292
|
dist: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY),
|
|
@@ -6000,16 +6296,32 @@ window.__ModuleLoader__.load({
|
|
|
6000
6296
|
px: viewRef.current.pos.x,
|
|
6001
6297
|
py: viewRef.current.pos.y
|
|
6002
6298
|
};
|
|
6299
|
+
return;
|
|
6300
|
+
}
|
|
6301
|
+
if (e.touches.length === 1) {
|
|
6302
|
+
pinchRef.current = null;
|
|
6303
|
+
if (!card.contains(e.target)) {
|
|
6304
|
+
touchDragRef.current = null;
|
|
6305
|
+
return;
|
|
6306
|
+
}
|
|
6307
|
+
const tp = e.touches[0];
|
|
6308
|
+
touchDragRef.current = {
|
|
6309
|
+
sx: tp.clientX,
|
|
6310
|
+
sy: tp.clientY,
|
|
6311
|
+
spx: viewRef.current.pos.x,
|
|
6312
|
+
spy: viewRef.current.pos.y
|
|
6313
|
+
};
|
|
6003
6314
|
}
|
|
6004
6315
|
}, []);
|
|
6005
6316
|
const onTouchMoveCb = (0, react.useCallback)((e) => {
|
|
6006
|
-
const
|
|
6007
|
-
if (!
|
|
6317
|
+
const card = containerRef.current;
|
|
6318
|
+
if (!card) return;
|
|
6008
6319
|
if (e.touches.length >= 2 && pinchRef.current !== null) {
|
|
6009
6320
|
e.preventDefault();
|
|
6321
|
+
movedRef.current = true;
|
|
6010
6322
|
const p = pinchRef.current;
|
|
6011
6323
|
if (p.dist <= 0) return;
|
|
6012
|
-
const rect =
|
|
6324
|
+
const rect = card.getBoundingClientRect();
|
|
6013
6325
|
const a = e.touches[0], b = e.touches[1];
|
|
6014
6326
|
const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
6015
6327
|
const cx = (a.clientX + b.clientX) / 2 - rect.left;
|
|
@@ -6026,6 +6338,7 @@ window.__ModuleLoader__.load({
|
|
|
6026
6338
|
e.preventDefault();
|
|
6027
6339
|
const d = touchDragRef.current;
|
|
6028
6340
|
const tp = e.touches[0];
|
|
6341
|
+
if (Math.abs(tp.clientX - d.sx) > 4 || Math.abs(tp.clientY - d.sy) > 4) movedRef.current = true;
|
|
6029
6342
|
setPos({
|
|
6030
6343
|
x: d.spx + tp.clientX - d.sx,
|
|
6031
6344
|
y: d.spy + tp.clientY - d.sy
|
|
@@ -6048,19 +6361,29 @@ window.__ModuleLoader__.load({
|
|
|
6048
6361
|
}
|
|
6049
6362
|
}, []);
|
|
6050
6363
|
(0, react.useEffect)(() => {
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6364
|
+
if (!overlayEl) return;
|
|
6365
|
+
const noPageGesture = (e) => e.preventDefault();
|
|
6366
|
+
overlayEl.addEventListener("wheel", onWheelCb, { passive: false });
|
|
6367
|
+
overlayEl.addEventListener("touchstart", onTouchStartCb, { passive: false });
|
|
6368
|
+
overlayEl.addEventListener("touchmove", onTouchMoveCb, { passive: false });
|
|
6369
|
+
overlayEl.addEventListener("touchend", onTouchEndCb, { passive: false });
|
|
6370
|
+
overlayEl.addEventListener("touchcancel", onTouchEndCb, { passive: false });
|
|
6371
|
+
overlayEl.addEventListener("gesturestart", noPageGesture, { passive: false });
|
|
6372
|
+
overlayEl.addEventListener("gesturechange", noPageGesture, { passive: false });
|
|
6373
|
+
overlayEl.addEventListener("gestureend", noPageGesture, { passive: false });
|
|
6057
6374
|
return () => {
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6375
|
+
overlayEl.removeEventListener("wheel", onWheelCb);
|
|
6376
|
+
overlayEl.removeEventListener("touchstart", onTouchStartCb);
|
|
6377
|
+
overlayEl.removeEventListener("touchmove", onTouchMoveCb);
|
|
6378
|
+
overlayEl.removeEventListener("touchend", onTouchEndCb);
|
|
6379
|
+
overlayEl.removeEventListener("touchcancel", onTouchEndCb);
|
|
6380
|
+
overlayEl.removeEventListener("gesturestart", noPageGesture);
|
|
6381
|
+
overlayEl.removeEventListener("gesturechange", noPageGesture);
|
|
6382
|
+
overlayEl.removeEventListener("gestureend", noPageGesture);
|
|
6062
6383
|
};
|
|
6063
6384
|
}, [
|
|
6385
|
+
overlayEl,
|
|
6386
|
+
onWheelCb,
|
|
6064
6387
|
onTouchStartCb,
|
|
6065
6388
|
onTouchMoveCb,
|
|
6066
6389
|
onTouchEndCb
|
|
@@ -6078,10 +6401,22 @@ window.__ModuleLoader__.load({
|
|
|
6078
6401
|
imgSize
|
|
6079
6402
|
]);
|
|
6080
6403
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6404
|
+
ref: setOverlayEl,
|
|
6081
6405
|
className: "dab-overlay",
|
|
6406
|
+
style: {
|
|
6407
|
+
touchAction: "none",
|
|
6408
|
+
overscrollBehavior: "contain"
|
|
6409
|
+
},
|
|
6082
6410
|
onClick: (e) => {
|
|
6411
|
+
if (movedRef.current) {
|
|
6412
|
+
movedRef.current = false;
|
|
6413
|
+
return;
|
|
6414
|
+
}
|
|
6083
6415
|
if (e.target === e.currentTarget) onClose();
|
|
6084
6416
|
},
|
|
6417
|
+
onMouseDown: () => {
|
|
6418
|
+
movedRef.current = false;
|
|
6419
|
+
},
|
|
6085
6420
|
children: [
|
|
6086
6421
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6087
6422
|
className: "dab-overlay-title",
|
|
@@ -6177,7 +6512,7 @@ window.__ModuleLoader__.load({
|
|
|
6177
6512
|
labelKey: "bgModeCenter"
|
|
6178
6513
|
}
|
|
6179
6514
|
];
|
|
6180
|
-
function BackgroundPage({ p }) {
|
|
6515
|
+
function BackgroundPage({ p, notify }) {
|
|
6181
6516
|
const { t, setWpFromServer, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, setRotation, addRotationItems, removeRotationItem, rotateNow, setVideoFromUrl, useStore } = p;
|
|
6182
6517
|
const storeUrl = useStore((s) => s.url);
|
|
6183
6518
|
const backgroundType = useStore((s) => s.backgroundType);
|
|
@@ -6194,7 +6529,11 @@ window.__ModuleLoader__.load({
|
|
|
6194
6529
|
const [urlVal, setUrlVal] = (0, react.useState)("");
|
|
6195
6530
|
const [urlBusy, setUrlBusy] = (0, react.useState)(false);
|
|
6196
6531
|
const [urlErr, setUrlErr] = (0, react.useState)(null);
|
|
6532
|
+
const bgRev = useStore((s) => s.bgRev);
|
|
6197
6533
|
const [mode, setModeState] = (0, react.useState)(rBgMode());
|
|
6534
|
+
(0, react.useEffect)(() => {
|
|
6535
|
+
setModeState(rBgMode());
|
|
6536
|
+
}, [bgRev]);
|
|
6198
6537
|
const isVideo = backgroundType === "video";
|
|
6199
6538
|
const isStatic = backgroundType === "image" || isVideo;
|
|
6200
6539
|
const isGenerated = !isStatic;
|
|
@@ -6208,14 +6547,15 @@ window.__ModuleLoader__.load({
|
|
|
6208
6547
|
genPreset,
|
|
6209
6548
|
generatedBg?.seed
|
|
6210
6549
|
]);
|
|
6211
|
-
const onFileSelect = (f) => {
|
|
6550
|
+
const onFileSelect = async (f) => {
|
|
6212
6551
|
if (f.type.startsWith("video/")) {
|
|
6213
|
-
setVideo(f, f.type);
|
|
6552
|
+
const outcome = await setVideo(f, f.type);
|
|
6553
|
+
if (!outcome.ok) notify(uploadRefusalText(outcome, t, "videoUploadFail"), false);
|
|
6214
6554
|
return;
|
|
6215
6555
|
}
|
|
6216
|
-
uploadWallpaper(f)
|
|
6217
|
-
|
|
6218
|
-
|
|
6556
|
+
const outcome = await uploadWallpaper(f);
|
|
6557
|
+
if (outcome.ok) setWpFromServer(WALLPAPER_SERVE_URL);
|
|
6558
|
+
else notify(uploadRefusalText(outcome, t, "bgUploadFail"), false);
|
|
6219
6559
|
};
|
|
6220
6560
|
const onDrop = (e) => {
|
|
6221
6561
|
e.preventDefault();
|
|
@@ -7489,31 +7829,37 @@ window.__ModuleLoader__.load({
|
|
|
7489
7829
|
//#endregion
|
|
7490
7830
|
//#region src/client/components/ThemeSection.tsx
|
|
7491
7831
|
/**
|
|
7492
|
-
* dsh-any-background —
|
|
7832
|
+
* dsh-any-background — appearance shell for the five pages.
|
|
7493
7833
|
*
|
|
7494
|
-
*
|
|
7495
|
-
*
|
|
7496
|
-
*
|
|
7497
|
-
*
|
|
7498
|
-
*
|
|
7499
|
-
*
|
|
7834
|
+
* Two surfaces render this shell, and only the chrome differs:
|
|
7835
|
+
* · `settings` — injected into the host settings dialog's content column,
|
|
7836
|
+
* which supplies the modal frame (nav rail on the left, brand block, page
|
|
7837
|
+
* max-width, the dialog's own scrolling);
|
|
7838
|
+
* · `sidebar` — a page inside dsh-better-sidebar, where the panel is much
|
|
7839
|
+
* narrower, is handed a full-height column, and has to own its own scroll
|
|
7840
|
+
* region. The brand block is dropped (the sidebar's tab bar already names
|
|
7841
|
+
* the page) and the rail turns into a compact row.
|
|
7842
|
+
* The five pages themselves never branch on the surface: width-driven
|
|
7843
|
+
* adaptation lives in the stylesheet's container queries, which measure the
|
|
7844
|
+
* shell itself rather than assuming either surface's width.
|
|
7500
7845
|
*/
|
|
7501
7846
|
function ThemeSection(props) {
|
|
7502
7847
|
ensureUiCss();
|
|
7503
7848
|
const { t } = props;
|
|
7849
|
+
const surface = props.surface ?? "settings";
|
|
7504
7850
|
const [page, setPage] = (0, react.useState)(0);
|
|
7505
7851
|
const [toast, setToast] = (0, react.useState)(null);
|
|
7506
7852
|
const toastTimer = (0, react.useRef)(void 0);
|
|
7507
7853
|
(0, react.useEffect)(() => () => window.clearTimeout(toastTimer.current), []);
|
|
7508
|
-
const notify = (msg, ok = true) => {
|
|
7854
|
+
const notify = (0, react.useCallback)((msg, ok = true) => {
|
|
7509
7855
|
setToast({
|
|
7510
7856
|
msg,
|
|
7511
7857
|
ok
|
|
7512
7858
|
});
|
|
7513
7859
|
window.clearTimeout(toastTimer.current);
|
|
7514
7860
|
toastTimer.current = window.setTimeout(() => setToast(null), 2600);
|
|
7515
|
-
};
|
|
7516
|
-
const pages = [
|
|
7861
|
+
}, []);
|
|
7862
|
+
const pages = (0, react.useMemo)(() => [
|
|
7517
7863
|
{
|
|
7518
7864
|
label: t("pageColor"),
|
|
7519
7865
|
Icon: DropletIcon,
|
|
@@ -7538,7 +7884,10 @@ window.__ModuleLoader__.load({
|
|
|
7538
7884
|
{
|
|
7539
7885
|
label: t("pageBackground"),
|
|
7540
7886
|
Icon: PhotoIcon,
|
|
7541
|
-
node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackgroundPage, {
|
|
7887
|
+
node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackgroundPage, {
|
|
7888
|
+
p: props,
|
|
7889
|
+
notify
|
|
7890
|
+
})
|
|
7542
7891
|
},
|
|
7543
7892
|
{
|
|
7544
7893
|
label: t("pageProfile"),
|
|
@@ -7548,16 +7897,21 @@ window.__ModuleLoader__.load({
|
|
|
7548
7897
|
notify
|
|
7549
7898
|
})
|
|
7550
7899
|
}
|
|
7551
|
-
]
|
|
7900
|
+
], [
|
|
7901
|
+
t,
|
|
7902
|
+
props,
|
|
7903
|
+
notify
|
|
7904
|
+
]);
|
|
7552
7905
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ErrorBoundary, {
|
|
7553
7906
|
t,
|
|
7554
7907
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7555
7908
|
className: "dab-root",
|
|
7909
|
+
"data-dab-surface": surface,
|
|
7556
7910
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7557
7911
|
className: "dab-shell",
|
|
7558
7912
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
|
|
7559
7913
|
className: "dab-nav",
|
|
7560
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7914
|
+
children: [surface === "sidebar" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7561
7915
|
className: "dab-brand",
|
|
7562
7916
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7563
7917
|
className: "dab-brand-tile",
|
|
@@ -7597,6 +7951,138 @@ window.__ModuleLoader__.load({
|
|
|
7597
7951
|
});
|
|
7598
7952
|
}
|
|
7599
7953
|
//#endregion
|
|
7954
|
+
//#region src/client/sidebar/store-hook.ts
|
|
7955
|
+
/**
|
|
7956
|
+
* dsh-any-background — selector hook over a host store instance.
|
|
7957
|
+
*
|
|
7958
|
+
* The settings slot kit hands the pages a `useStore(selector)` hook that is
|
|
7959
|
+
* bound to this plugin's store instance by the host renderer. A better-sidebar
|
|
7960
|
+
* page gets no such kit — its tab props carry `ctx`/`store`/`scope` for
|
|
7961
|
+
* better-sidebar's own store — so the same contract is rebuilt here from the
|
|
7962
|
+
* store instance itself, which already IS an observable source
|
|
7963
|
+
* (`getSnapshot`/`subscribe`, see `defineStore` in the host's client store).
|
|
7964
|
+
*
|
|
7965
|
+
* Semantics match the host binding on purpose: reduce the published snapshot
|
|
7966
|
+
* through the selector and compare with `Object.is`, so a page subscribing to
|
|
7967
|
+
* one field does not re-render on unrelated writes.
|
|
7968
|
+
*/
|
|
7969
|
+
/**
|
|
7970
|
+
* Bind a store instance to a `useStore(selector)` hook. Call once per store and
|
|
7971
|
+
* share the result: the returned hook keeps one subscription identity for its
|
|
7972
|
+
* whole lifetime.
|
|
7973
|
+
* @param store - observable store instance (`defineStore` handle).
|
|
7974
|
+
* @returns a selector hook reading that instance.
|
|
7975
|
+
*/
|
|
7976
|
+
function createStoreHook(store) {
|
|
7977
|
+
const subscribe = (onChange) => store.subscribe(onChange);
|
|
7978
|
+
const { getSnapshot } = store;
|
|
7979
|
+
return function useStoreSelector(selector) {
|
|
7980
|
+
const cache = (0, react.useRef)(null);
|
|
7981
|
+
const read = (0, react.useCallback)(() => {
|
|
7982
|
+
const snapshot = getSnapshot();
|
|
7983
|
+
const hit = cache.current;
|
|
7984
|
+
if (hit !== null && hit.snapshot === snapshot) return hit.value;
|
|
7985
|
+
const value = selector(snapshot);
|
|
7986
|
+
cache.current = {
|
|
7987
|
+
snapshot,
|
|
7988
|
+
value
|
|
7989
|
+
};
|
|
7990
|
+
return value;
|
|
7991
|
+
}, [selector]);
|
|
7992
|
+
return (0, react.useSyncExternalStore)(subscribe, read, read);
|
|
7993
|
+
};
|
|
7994
|
+
}
|
|
7995
|
+
//#endregion
|
|
7996
|
+
//#region src/client/sidebar/tab.tsx
|
|
7997
|
+
/**
|
|
7998
|
+
* dsh-any-background — dsh-better-sidebar integration.
|
|
7999
|
+
*
|
|
8000
|
+
* Publishes the plugin's appearance controls as a sidebar page ("主题") in
|
|
8001
|
+
* better-sidebar's tab registry. The page renders the SAME five-page tree as
|
|
8002
|
+
* the settings section — `ThemeSection` takes a `surface` variant — so there is
|
|
8003
|
+
* one UI to maintain, not two forks of it.
|
|
8004
|
+
*
|
|
8005
|
+
* The dependency is optional at every level, which is why nothing here imports
|
|
8006
|
+
* the package:
|
|
8007
|
+
* · the service is read off the client context at runtime, so a DSH install
|
|
8008
|
+
* without better-sidebar silently registers nothing, and a better-sidebar
|
|
8009
|
+
* that mounts *after* this plugin still gets the page (`ctx.inject` fires
|
|
8010
|
+
* whenever the service shows up, never when it never does);
|
|
8011
|
+
* · the plugin's own types are restated in the minimal local contract below
|
|
8012
|
+
* instead of imported — a value import would be rejected by the host's
|
|
8013
|
+
* client-bundle purity gate, and even a type-only import would make the
|
|
8014
|
+
* package a build-time dependency for everyone who does not run it.
|
|
8015
|
+
*/
|
|
8016
|
+
/**
|
|
8017
|
+
* The active locale id, used as a re-render signal: page labels are resolved by
|
|
8018
|
+
* calling `t(...)` at render time, so the props object (and with it the memoized
|
|
8019
|
+
* page tree) has to be rebuilt when the language flips — otherwise the page
|
|
8020
|
+
* would keep painting the previous language until some unrelated store write
|
|
8021
|
+
* happened to re-render it.
|
|
8022
|
+
* @param locale - host locale service.
|
|
8023
|
+
* @returns the active locale id (a stable string, safe for `Object.is`).
|
|
8024
|
+
*/
|
|
8025
|
+
function useActiveLocale(locale) {
|
|
8026
|
+
const subscribe = (0, react.useCallback)((onChange) => locale.subscribe(onChange), [locale]);
|
|
8027
|
+
const read = (0, react.useCallback)(() => locale.getSnapshot().active, [locale]);
|
|
8028
|
+
return (0, react.useSyncExternalStore)(subscribe, read, read);
|
|
8029
|
+
}
|
|
8030
|
+
function ThemeTab({ face, useStore, locale }) {
|
|
8031
|
+
const active = useActiveLocale(locale);
|
|
8032
|
+
const props = (0, react.useMemo)(() => ({
|
|
8033
|
+
...face,
|
|
8034
|
+
useStore
|
|
8035
|
+
}), [
|
|
8036
|
+
face,
|
|
8037
|
+
useStore,
|
|
8038
|
+
active
|
|
8039
|
+
]);
|
|
8040
|
+
return (0, react.createElement)(ThemeSection, {
|
|
8041
|
+
...props,
|
|
8042
|
+
surface: "sidebar"
|
|
8043
|
+
});
|
|
8044
|
+
}
|
|
8045
|
+
/**
|
|
8046
|
+
* Register the appearance page with better-sidebar, if it is installed.
|
|
8047
|
+
*
|
|
8048
|
+
* Call from the client `apply`. The registration is owned by the injected
|
|
8049
|
+
* scope's effect, so unloading the plugin (or HMR) withdraws it — without that,
|
|
8050
|
+
* the next activation would throw on the duplicate id.
|
|
8051
|
+
* @param ctx - client context.
|
|
8052
|
+
* @param opts.face - lazy accessor for the shared business face.
|
|
8053
|
+
* @param opts.store - the plugin's store instance, or null on a host where the
|
|
8054
|
+
* store module never resolved (in that case there is nothing to bind and no
|
|
8055
|
+
* page is registered).
|
|
8056
|
+
*/
|
|
8057
|
+
function registerThemeSidebarTab(ctx, opts) {
|
|
8058
|
+
const { store } = opts;
|
|
8059
|
+
if (store === null || typeof store.getSnapshot !== "function" || typeof store.subscribe !== "function") {
|
|
8060
|
+
console.warn("dsh-any-background: no observable store instance on this host; the sidebar page stays unregistered");
|
|
8061
|
+
return;
|
|
8062
|
+
}
|
|
8063
|
+
const locale = ctx.locale;
|
|
8064
|
+
const t = locale.bind(NS);
|
|
8065
|
+
const useStore = createStoreHook(store);
|
|
8066
|
+
const ORDER = 55;
|
|
8067
|
+
ctx.inject?.(["betterSidebar"], (scope) => {
|
|
8068
|
+
const service = scope.betterSidebar;
|
|
8069
|
+
if (service === void 0) return;
|
|
8070
|
+
const face = opts.face();
|
|
8071
|
+
scope.effect(() => service.registerTab({
|
|
8072
|
+
id: "dsh-any-background:theme",
|
|
8073
|
+
title: () => t("nav"),
|
|
8074
|
+
icon: (size) => (0, react.createElement)(SunIcon, { size }),
|
|
8075
|
+
order: ORDER,
|
|
8076
|
+
single: true,
|
|
8077
|
+
component: () => (0, react.createElement)(ThemeTab, {
|
|
8078
|
+
face,
|
|
8079
|
+
useStore,
|
|
8080
|
+
locale
|
|
8081
|
+
})
|
|
8082
|
+
}));
|
|
8083
|
+
});
|
|
8084
|
+
}
|
|
8085
|
+
//#endregion
|
|
7600
8086
|
//#region src/client/index.tsx
|
|
7601
8087
|
/**
|
|
7602
8088
|
* dsh-any-background — browser half entry.
|
|
@@ -7617,6 +8103,7 @@ window.__ModuleLoader__.load({
|
|
|
7617
8103
|
initRpc((endpoint, payload) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload).then((res) => res));
|
|
7618
8104
|
const [initH, initS, initL] = rColor();
|
|
7619
8105
|
let customDispose = null;
|
|
8106
|
+
let registerFailed = false;
|
|
7620
8107
|
const registerCustom = (h, s, l) => {
|
|
7621
8108
|
customDispose?.();
|
|
7622
8109
|
try {
|
|
@@ -7649,23 +8136,47 @@ window.__ModuleLoader__.load({
|
|
|
7649
8136
|
colorScheme,
|
|
7650
8137
|
tokens
|
|
7651
8138
|
});
|
|
7652
|
-
} catch {
|
|
8139
|
+
} catch (e) {
|
|
8140
|
+
if (!(() => {
|
|
8141
|
+
try {
|
|
8142
|
+
return ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
|
|
8143
|
+
} catch {
|
|
8144
|
+
return false;
|
|
8145
|
+
}
|
|
8146
|
+
})()) {
|
|
8147
|
+
if (!registerFailed) {
|
|
8148
|
+
registerFailed = true;
|
|
8149
|
+
console.error("dsh-any-background: host theme register failed; the custom skin will stay inactive", e);
|
|
8150
|
+
}
|
|
8151
|
+
customDispose = null;
|
|
8152
|
+
return false;
|
|
8153
|
+
}
|
|
7653
8154
|
customDispose = null;
|
|
7654
8155
|
}
|
|
7655
|
-
const present =
|
|
8156
|
+
const present = (() => {
|
|
8157
|
+
try {
|
|
8158
|
+
return ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
|
|
8159
|
+
} catch {
|
|
8160
|
+
return false;
|
|
8161
|
+
}
|
|
8162
|
+
})();
|
|
7656
8163
|
if (present) ctx.theme.setTheme(CUSTOM_ID);
|
|
7657
8164
|
return present;
|
|
7658
8165
|
};
|
|
7659
8166
|
if (rHasColor()) registerCustom(initH, initS, initL);
|
|
7660
|
-
onVerdictApplied(() => {
|
|
8167
|
+
const disposeVerdict = onVerdictApplied(() => {
|
|
7661
8168
|
if (!rHasColor()) registerCustom();
|
|
7662
8169
|
});
|
|
7663
|
-
onColorAdopted((hsl) => {
|
|
8170
|
+
const disposeColorAdopted = onColorAdopted((hsl) => {
|
|
7664
8171
|
registerCustom(hsl[0], hsl[1], hsl[2]);
|
|
7665
8172
|
saveConfig();
|
|
7666
8173
|
colorRev++;
|
|
7667
8174
|
bound?.syncColor(hslToHsv(hsl[0], hsl[1], hsl[2]), colorRev);
|
|
7668
8175
|
});
|
|
8176
|
+
ctx.effect(() => () => {
|
|
8177
|
+
disposeVerdict();
|
|
8178
|
+
disposeColorAdopted();
|
|
8179
|
+
}, "dsh-any-background: verdict/color listeners");
|
|
7669
8180
|
ctx.effect(() => () => {
|
|
7670
8181
|
customDispose?.();
|
|
7671
8182
|
if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
|
|
@@ -7683,7 +8194,7 @@ window.__ModuleLoader__.load({
|
|
|
7683
8194
|
let colorRev = 0;
|
|
7684
8195
|
let bgRev = 0;
|
|
7685
8196
|
const colorTimerRef = { current: null };
|
|
7686
|
-
const
|
|
8197
|
+
const storeSpec = defineStore === null ? null : defineStore({
|
|
7687
8198
|
init: () => ({
|
|
7688
8199
|
url: null,
|
|
7689
8200
|
rev: -1,
|
|
@@ -7734,13 +8245,44 @@ window.__ModuleLoader__.load({
|
|
|
7734
8245
|
}
|
|
7735
8246
|
}
|
|
7736
8247
|
});
|
|
8248
|
+
/** One shared instance, whatever the host build handed back.
|
|
8249
|
+
*
|
|
8250
|
+
* The plugin publishes a single appearance state that BOTH surfaces read —
|
|
8251
|
+
* the settings section (through the renderer's `useStore`) and the
|
|
8252
|
+
* better-sidebar page (through the hook in `sidebar/store-hook`). Handing the
|
|
8253
|
+
* host a declaration whose `create` always returns this very instance is what
|
|
8254
|
+
* keeps them from drifting: letting the renderer mint its own would give the
|
|
8255
|
+
* sidebar a second, permanently empty copy of the state. */
|
|
8256
|
+
const storeInstance = storeSpec === null ? null : typeof storeSpec.getSnapshot === "function" ? storeSpec : storeSpec.create();
|
|
8257
|
+
/** What gets registered: the instance on newer builds, a single-instance
|
|
8258
|
+
* declaration on the ones that expect a declaration. */
|
|
8259
|
+
const store = storeSpec === null || storeInstance === storeSpec ? storeSpec : {
|
|
8260
|
+
spec: storeSpec.spec ?? {},
|
|
8261
|
+
create: () => storeInstance
|
|
8262
|
+
};
|
|
7737
8263
|
let bound = null;
|
|
8264
|
+
if (storeInstance !== null) bound = storeInstance.actions;
|
|
7738
8265
|
const syncBg = () => {
|
|
7739
8266
|
rev++;
|
|
7740
8267
|
bgRev++;
|
|
7741
8268
|
bound?.syncBg(rWp(), rev, cfg.backgroundType, cfg.generatedBg, bgRev, cfg.regenerateOnReload);
|
|
7742
8269
|
};
|
|
7743
|
-
onGeneratedSnapshot(syncBg);
|
|
8270
|
+
const disposeSnapshot = onGeneratedSnapshot(syncBg);
|
|
8271
|
+
ctx.effect(() => () => disposeSnapshot(), "dsh-any-background: snapshot listener");
|
|
8272
|
+
/** Capture the first frame of the video now applied and finish its
|
|
8273
|
+
* adaptation: the frame stands in for every still-image API (previews,
|
|
8274
|
+
* color extraction) and the brightness verdict follows it. The stale guard
|
|
8275
|
+
* skips a capture whose video was swapped out from under it (another
|
|
8276
|
+
* upload or a rotation switch landing mid-decode). */
|
|
8277
|
+
const captureAndApplyVideo = (url) => {
|
|
8278
|
+
if (url === null) return;
|
|
8279
|
+
captureVideoSnapshot(url).then((snap) => {
|
|
8280
|
+
if (rWpVideo() !== url) return;
|
|
8281
|
+
setWpVideoSnapshot(snap);
|
|
8282
|
+
applyThemeColor();
|
|
8283
|
+
syncBg();
|
|
8284
|
+
});
|
|
8285
|
+
};
|
|
7744
8286
|
let metaRev = 0;
|
|
7745
8287
|
const syncMetaNow = () => {
|
|
7746
8288
|
metaRev++;
|
|
@@ -7861,7 +8403,7 @@ window.__ModuleLoader__.load({
|
|
|
7861
8403
|
const scheduleTick = () => {
|
|
7862
8404
|
const sc = rSchedule();
|
|
7863
8405
|
if (!sc.enabled) return;
|
|
7864
|
-
const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)")
|
|
8406
|
+
const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ?? false : isNightNow(sc)) ? sc.nightProfile : sc.dayProfile;
|
|
7865
8407
|
if (!want || want === cfg.activeProfile) return;
|
|
7866
8408
|
applyProfileById(want);
|
|
7867
8409
|
};
|
|
@@ -7889,12 +8431,7 @@ window.__ModuleLoader__.load({
|
|
|
7889
8431
|
if (cfg.backgroundType === "video") {
|
|
7890
8432
|
const v = rWpVideo();
|
|
7891
8433
|
if (v) {
|
|
7892
|
-
|
|
7893
|
-
if (rWpVideo() !== v) return;
|
|
7894
|
-
setWpVideoSnapshot(snap);
|
|
7895
|
-
applyThemeColor();
|
|
7896
|
-
syncBg();
|
|
7897
|
-
});
|
|
8434
|
+
captureAndApplyVideo(v);
|
|
7898
8435
|
applyWp();
|
|
7899
8436
|
} else {
|
|
7900
8437
|
cfg.backgroundType = "image";
|
|
@@ -7944,19 +8481,25 @@ window.__ModuleLoader__.load({
|
|
|
7944
8481
|
document.body.append(sentinel);
|
|
7945
8482
|
const viewportObserver = new ResizeObserver(applySoon);
|
|
7946
8483
|
viewportObserver.observe(sentinel);
|
|
7947
|
-
const dprQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
7948
|
-
dprQuery
|
|
8484
|
+
const dprQuery = window.matchMedia?.(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
8485
|
+
dprQuery?.addEventListener?.("change", applySoon);
|
|
7949
8486
|
ctx.effect(() => () => {
|
|
7950
8487
|
viewportObserver.disconnect();
|
|
7951
|
-
dprQuery
|
|
8488
|
+
dprQuery?.removeEventListener?.("change", applySoon);
|
|
7952
8489
|
sentinel.remove();
|
|
7953
8490
|
}, "dsh-any-background: viewport watch");
|
|
7954
8491
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
7955
8492
|
zh,
|
|
7956
8493
|
en
|
|
7957
8494
|
}), "dsh-any-background: i18n");
|
|
7958
|
-
|
|
7959
|
-
|
|
8495
|
+
/** The business face, built once on first use and shared by both surfaces
|
|
8496
|
+
* (the settings section and the better-sidebar page). Memoizing is not just
|
|
8497
|
+
* about cost: the face closes over per-surface state — the font preview blob
|
|
8498
|
+
* URL behind releaseFontPreview — so a second build would give the sidebar
|
|
8499
|
+
* its own preview slot and leak whichever copy never gets released. */
|
|
8500
|
+
let themeFace = null;
|
|
8501
|
+
const buildFace = () => {
|
|
8502
|
+
if (themeFace !== null) return themeFace;
|
|
7960
8503
|
syncBg();
|
|
7961
8504
|
syncMetaNow();
|
|
7962
8505
|
const playVideoFromBlob = (blob, mime) => {
|
|
@@ -7967,15 +8510,10 @@ window.__ModuleLoader__.load({
|
|
|
7967
8510
|
setWpVideoUrl(localUrl, mime ?? blob.type ?? "video/mp4");
|
|
7968
8511
|
applyWp();
|
|
7969
8512
|
syncBg();
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
if (
|
|
7973
|
-
|
|
7974
|
-
applyThemeColor();
|
|
7975
|
-
syncBg();
|
|
7976
|
-
});
|
|
7977
|
-
uploadVideo(blob, mime ?? blob.type ?? "video/mp4").then((ok) => {
|
|
7978
|
-
if (ok) persistConfig();
|
|
8513
|
+
captureAndApplyVideo(rWpVideo());
|
|
8514
|
+
return uploadVideo(blob, mime ?? blob.type ?? "video/mp4").then((outcome) => {
|
|
8515
|
+
if (outcome.ok) persistConfig();
|
|
8516
|
+
return outcome;
|
|
7979
8517
|
});
|
|
7980
8518
|
};
|
|
7981
8519
|
const FONT_EXT_MIME = {
|
|
@@ -8002,7 +8540,7 @@ window.__ModuleLoader__.load({
|
|
|
8002
8540
|
};
|
|
8003
8541
|
const [wh, ws, wl] = rColor();
|
|
8004
8542
|
const [dh, ds, dv] = hslToHsv(wh, ws, wl);
|
|
8005
|
-
|
|
8543
|
+
const built = {
|
|
8006
8544
|
t: ctx.locale.bind(NS),
|
|
8007
8545
|
hue: dh,
|
|
8008
8546
|
sat: ds,
|
|
@@ -8052,13 +8590,11 @@ window.__ModuleLoader__.load({
|
|
|
8052
8590
|
applyThemeColor();
|
|
8053
8591
|
syncBg();
|
|
8054
8592
|
saveConfig();
|
|
8055
|
-
return;
|
|
8056
|
-
}
|
|
8057
|
-
if (typeof u !== "string") {
|
|
8058
|
-
playVideoFromBlob(u, mime);
|
|
8059
|
-
return;
|
|
8593
|
+
return { ok: true };
|
|
8060
8594
|
}
|
|
8061
|
-
|
|
8595
|
+
if (typeof u !== "string") return playVideoFromBlob(u, mime);
|
|
8596
|
+
const ok = await persistVideo(u);
|
|
8597
|
+
const live = ok ? VIDEO_SERVE_URL : u;
|
|
8062
8598
|
cfg.backgroundType = "video";
|
|
8063
8599
|
setWpUrl(null);
|
|
8064
8600
|
cfg.videoBgState = { ...DEFAULT_CONFIG.bgState };
|
|
@@ -8066,13 +8602,8 @@ window.__ModuleLoader__.load({
|
|
|
8066
8602
|
applyWp();
|
|
8067
8603
|
saveConfig();
|
|
8068
8604
|
syncBg();
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
if (rWpVideo() !== applied) return;
|
|
8072
|
-
setWpVideoSnapshot(snap);
|
|
8073
|
-
applyThemeColor();
|
|
8074
|
-
syncBg();
|
|
8075
|
-
});
|
|
8605
|
+
captureAndApplyVideo(rWpVideo());
|
|
8606
|
+
return { ok };
|
|
8076
8607
|
},
|
|
8077
8608
|
setBgType: (type) => {
|
|
8078
8609
|
setBackgroundType(type);
|
|
@@ -8116,18 +8647,21 @@ window.__ModuleLoader__.load({
|
|
|
8116
8647
|
const localUrl = URL.createObjectURL(file);
|
|
8117
8648
|
fontPreviewUrl = localUrl;
|
|
8118
8649
|
applyFontFace(localUrl, true, fontMimeFromName(file.name));
|
|
8119
|
-
const
|
|
8120
|
-
if (mime ===
|
|
8650
|
+
const outcome = await uploadFont(file);
|
|
8651
|
+
if (!outcome.ok || outcome.mime === void 0) {
|
|
8121
8652
|
applyStoredFont();
|
|
8122
8653
|
releaseFontPreview(4e3);
|
|
8123
|
-
return
|
|
8654
|
+
return {
|
|
8655
|
+
ok: false,
|
|
8656
|
+
refusal: outcome.refusal
|
|
8657
|
+
};
|
|
8124
8658
|
}
|
|
8125
|
-
cfg.fontMime = mime;
|
|
8659
|
+
cfg.fontMime = outcome.mime;
|
|
8126
8660
|
cfg.fontEnabled = true;
|
|
8127
|
-
applyFontFace(FONT_SERVE_URL, true, mime);
|
|
8661
|
+
applyFontFace(FONT_SERVE_URL, true, outcome.mime);
|
|
8128
8662
|
releaseFontPreview(4e3);
|
|
8129
8663
|
persistConfig();
|
|
8130
|
-
return true;
|
|
8664
|
+
return { ok: true };
|
|
8131
8665
|
},
|
|
8132
8666
|
removeFont: () => {
|
|
8133
8667
|
releaseFontPreview(0);
|
|
@@ -8213,7 +8747,7 @@ window.__ModuleLoader__.load({
|
|
|
8213
8747
|
a.href = url;
|
|
8214
8748
|
a.download = "dsh-any-theme.json";
|
|
8215
8749
|
a.click();
|
|
8216
|
-
URL.revokeObjectURL(url);
|
|
8750
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 4e3);
|
|
8217
8751
|
},
|
|
8218
8752
|
importTheme: async (file) => {
|
|
8219
8753
|
try {
|
|
@@ -8233,16 +8767,9 @@ window.__ModuleLoader__.load({
|
|
|
8233
8767
|
}
|
|
8234
8768
|
if (blob !== null) playVideoFromBlob(blob, cfg.videoMime);
|
|
8235
8769
|
else {
|
|
8236
|
-
|
|
8237
|
-
setWpVideoUrl(live, cfg.videoMime);
|
|
8770
|
+
setWpVideoUrl(await persistVideo(video) ? VIDEO_SERVE_URL : video, cfg.videoMime);
|
|
8238
8771
|
applyWp();
|
|
8239
|
-
|
|
8240
|
-
captureVideoSnapshot(live).then((snap) => {
|
|
8241
|
-
if (rWpVideo() !== applied) return;
|
|
8242
|
-
setWpVideoSnapshot(snap);
|
|
8243
|
-
applyThemeColor();
|
|
8244
|
-
syncBg();
|
|
8245
|
-
});
|
|
8772
|
+
captureAndApplyVideo(rWpVideo());
|
|
8246
8773
|
}
|
|
8247
8774
|
} else {
|
|
8248
8775
|
setWpVideoUrl(null, null);
|
|
@@ -8401,18 +8928,19 @@ window.__ModuleLoader__.load({
|
|
|
8401
8928
|
applyWp();
|
|
8402
8929
|
saveConfig();
|
|
8403
8930
|
syncBg();
|
|
8404
|
-
|
|
8405
|
-
captureVideoSnapshot(applied).then((snap) => {
|
|
8406
|
-
if (rWpVideo() !== applied) return;
|
|
8407
|
-
setWpVideoSnapshot(snap);
|
|
8408
|
-
applyThemeColor();
|
|
8409
|
-
syncBg();
|
|
8410
|
-
});
|
|
8931
|
+
captureAndApplyVideo(rWpVideo());
|
|
8411
8932
|
return true;
|
|
8412
8933
|
}
|
|
8413
8934
|
};
|
|
8935
|
+
themeFace = built;
|
|
8936
|
+
return built;
|
|
8414
8937
|
};
|
|
8415
|
-
|
|
8938
|
+
const sectionInject = (actions) => {
|
|
8939
|
+
bound = actions;
|
|
8940
|
+
return buildFace();
|
|
8941
|
+
};
|
|
8942
|
+
if (store === null) console.error("dsh-any-background: settings panel disabled on this host (no store module)");
|
|
8943
|
+
else ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
8416
8944
|
name: "settings.section",
|
|
8417
8945
|
id: "dsh-any-background",
|
|
8418
8946
|
order: 35,
|
|
@@ -8421,6 +8949,10 @@ window.__ModuleLoader__.load({
|
|
|
8421
8949
|
store,
|
|
8422
8950
|
inject: sectionInject
|
|
8423
8951
|
}, ThemeSection));
|
|
8952
|
+
registerThemeSidebarTab(ctx, {
|
|
8953
|
+
face: buildFace,
|
|
8954
|
+
store: storeInstance
|
|
8955
|
+
});
|
|
8424
8956
|
const navLabel = () => ctx.locale.bind(NS)("nav");
|
|
8425
8957
|
const applyNavIcon = () => {
|
|
8426
8958
|
const nav = document.querySelector("[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]")?.querySelector("nav");
|