dsh-any-background 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -48,7 +48,7 @@ window.__ModuleLoader__.load({
48
48
  bgChoose: "选择图片或视频",
49
49
  bgRemove: "移除背景",
50
50
  bgFromUrl: "从网址",
51
- bgUrlPlaceholder: "粘贴图片网址 https://…",
51
+ bgUrlPlaceholder: "粘贴图片或视频网址 https://…",
52
52
  bgUrlApply: "应用",
53
53
  bgUrlCancel: "取消",
54
54
  bgUrlApplying: "加载中…",
@@ -122,6 +122,13 @@ window.__ModuleLoader__.load({
122
122
  presetDots: "点阵",
123
123
  presetWaves: "波浪",
124
124
  presetPoly: "低多边形",
125
+ presetStarfield: "星空",
126
+ presetRain: "雨滴",
127
+ presetContour: "等高线",
128
+ presetMeta: "流体光斑",
129
+ bgPause: "暂停",
130
+ bgResume: "播放",
131
+ bgUrlVideoFail: "获取视频失败,请确认链接可直接下载视频文件",
125
132
  schemeTitle: "界面明暗",
126
133
  schemeLight: "亮色",
127
134
  schemeDark: "暗色",
@@ -204,7 +211,7 @@ window.__ModuleLoader__.load({
204
211
  bgChoose: "Choose image or video",
205
212
  bgRemove: "Remove background",
206
213
  bgFromUrl: "From URL",
207
- bgUrlPlaceholder: "Paste an image URL https://…",
214
+ bgUrlPlaceholder: "Paste an image or video URL https://…",
208
215
  bgUrlApply: "Apply",
209
216
  bgUrlCancel: "Cancel",
210
217
  bgUrlApplying: "Loading…",
@@ -278,6 +285,13 @@ window.__ModuleLoader__.load({
278
285
  presetDots: "Dots",
279
286
  presetWaves: "Waves",
280
287
  presetPoly: "Low poly",
288
+ presetStarfield: "Starfield",
289
+ presetRain: "Rain",
290
+ presetContour: "Contours",
291
+ presetMeta: "Fluid orbs",
292
+ bgPause: "Pause",
293
+ bgResume: "Play",
294
+ bgUrlVideoFail: "Could not fetch the video — make sure the link downloads a video file directly",
281
295
  schemeTitle: "Interface scheme",
282
296
  schemeLight: "Light",
283
297
  schemeDark: "Dark",
@@ -408,12 +422,19 @@ window.__ModuleLoader__.load({
408
422
  let wpVideoSnapshot = null;
409
423
  /** Local blob URL backing an in-session video; revoked when replaced or cleared. */
410
424
  let wpVideoObjectUrl = null;
411
- function setWpImageUrl(url) {
412
- wpImageUrl = url;
413
- }
414
425
  function setWpUrl(url) {
415
426
  wpUrl = url;
416
427
  }
428
+ let imageRev = 0;
429
+ function bumpImageRev(url) {
430
+ if (url === null) return url;
431
+ if (!/^https?:\/\//i.test(url) && !url.startsWith("/")) return url;
432
+ imageRev++;
433
+ return `${url}${url.includes("?") ? "&" : "?"}r=${imageRev}`;
434
+ }
435
+ function setWpImageUrl(url) {
436
+ wpImageUrl = bumpImageRev(url);
437
+ }
417
438
  let videoRev = 0;
418
439
  function setWpVideoUrl(url, mime) {
419
440
  if (url === null) {
@@ -748,7 +769,8 @@ window.__ModuleLoader__.load({
748
769
  preset: [
749
770
  "aurora",
750
771
  "nebula",
751
- "noise"
772
+ "noise",
773
+ "starfield"
752
774
  ].includes(p.preset) ? p.preset : "aurora",
753
775
  speed: cl(p.speed, 0, 2, .3),
754
776
  scale: cl(p.scale, .3, 3, 1),
@@ -759,7 +781,10 @@ window.__ModuleLoader__.load({
759
781
  preset: [
760
782
  "dots",
761
783
  "waves",
762
- "poly"
784
+ "poly",
785
+ "rain",
786
+ "contour",
787
+ "meta"
763
788
  ].includes(p.preset) ? p.preset : "dots",
764
789
  density: cl(p.density, 0, 1, .5),
765
790
  scale: cl(p.scale, .3, 3, 1),
@@ -771,6 +796,10 @@ window.__ModuleLoader__.load({
771
796
  const RPC_CHANNEL = "/dsh-any-background";
772
797
  /** Same-origin serve URL of the persisted video (enough for <video src>/fetch). */
773
798
  const VIDEO_SERVE_URL = "/dsh-any-background/video";
799
+ /** Same-origin serve URL of the persisted wallpaper (native <img> loading). */
800
+ const WALLPAPER_SERVE_URL = "/dsh-any-background/wallpaper";
801
+ /** Raw-bytes upload endpoint for the wallpaper slot (no base64 inflation). */
802
+ const WALLPAPER_UPLOAD_URL = "/dsh-any-background/wallpaper/upload";
774
803
  /** HTTP route new videos are POSTed to as raw bytes (see uploadVideo). */
775
804
  const VIDEO_UPLOAD_URL = "/dsh-any-background/video/upload";
776
805
  const RPC_NS = "dshAnyBackground";
@@ -810,19 +839,20 @@ window.__ModuleLoader__.load({
810
839
  function persistConfig() {
811
840
  rpcCall("writeConfig", { config: cfg });
812
841
  }
813
- /** Load the persisted theme (config + wallpaper + video URL) from the node half.
814
- * Resolves true when the server advanced a due wallpaper rotation during the
815
- * read — the restored wallpaper is then already the new pick. */
842
+ /** Load the persisted theme (config + wallpaper URL + video URL) from the node
843
+ * half. Image and video both travel as serve URLs — never bytes — so this RPC
844
+ * stays tiny. Resolves true when the server advanced a due wallpaper rotation
845
+ * during the read — the restored wallpaper is then already the new pick. */
816
846
  async function loadPersisted() {
817
847
  const data = await rpcCall("read", {});
818
848
  if (data && typeof data === "object") {
819
849
  const d = data;
820
850
  if (d.config) adoptConfig(d.config);
821
- if (typeof d.wallpaper === "string") setWpImageUrl(d.wallpaper);
822
- else if (d.wallpaper === null) setWpImageUrl(null);
851
+ if (typeof d.wallpaperUrl === "string") setWpImageUrl(d.wallpaperUrl);
852
+ else if (d.wallpaperUrl === null) setWpImageUrl(null);
823
853
  if (typeof d.videoUrl === "string") setWpVideoUrl(d.videoUrl, cfg.videoMime);
824
854
  else if (d.videoUrl === null) setWpVideoUrl(null, null);
825
- if (cfg.backgroundType === "image") setWpUrl(d.wallpaper === null ? null : d.wallpaper);
855
+ if (cfg.backgroundType === "image") setWpUrl(rWpImage());
826
856
  return d.rotated === true;
827
857
  }
828
858
  return false;
@@ -837,7 +867,7 @@ window.__ModuleLoader__.load({
837
867
  return await rpcCall("setVideo", { dataUrl }) === true;
838
868
  }
839
869
  /** Download a wallpaper from a network URL and persist it into the local slot
840
- * (the host replaces wallpaper.jpg). Returns the freshly stored data-URL on
870
+ * (the host replaces wallpaper.jpg). Returns the freshly stored serve URL on
841
871
  * success, or the host's failure message. */
842
872
  async function setWallpaperFromUrl(url) {
843
873
  if (!rpcCallFn) return {
@@ -864,7 +894,40 @@ window.__ModuleLoader__.load({
864
894
  const v = res.value;
865
895
  return v?.ok === true ? {
866
896
  ok: true,
867
- dataUrl: v.dataUrl ?? null
897
+ wallpaperUrl: v.wallpaperUrl ?? null
898
+ } : {
899
+ ok: false,
900
+ error: v?.error ?? "failed"
901
+ };
902
+ }
903
+ /** Download a background video from a network URL. The server streams it into
904
+ * the video slot and returns the resolved MIME for playback via the serve URL. */
905
+ async function setVideoFromUrl(url) {
906
+ if (!rpcCallFn) return {
907
+ ok: false,
908
+ error: "rpc not ready"
909
+ };
910
+ let res;
911
+ try {
912
+ res = await rpcCallFn(rpcEndpoint("setVideoUrl"), { url });
913
+ } catch (e) {
914
+ return {
915
+ ok: false,
916
+ error: e instanceof Error ? e.message : String(e)
917
+ };
918
+ }
919
+ if (!res) return {
920
+ ok: false,
921
+ error: "no response"
922
+ };
923
+ if (res.ok !== true) return {
924
+ ok: false,
925
+ error: res.error?.message ?? "request failed"
926
+ };
927
+ const v = res.value;
928
+ return v?.ok === true ? {
929
+ ok: true,
930
+ mime: v.mime ?? "video/mp4"
868
931
  } : {
869
932
  ok: false,
870
933
  error: v?.error ?? "failed"
@@ -884,6 +947,20 @@ window.__ModuleLoader__.load({
884
947
  return false;
885
948
  }
886
949
  }
950
+ /** Upload a wallpaper's raw bytes over HTTP — same streaming model as videos,
951
+ * original pixels preserved, zero base64 round-trips. */
952
+ async function uploadWallpaper(blob) {
953
+ try {
954
+ return (await fetch(WALLPAPER_UPLOAD_URL, {
955
+ method: "POST",
956
+ headers: { "Content-Type": blob.type || "image/jpeg" },
957
+ body: blob
958
+ })).ok;
959
+ } catch (e) {
960
+ console.warn("dsh-any-background: wallpaper upload failed", e);
961
+ return false;
962
+ }
963
+ }
887
964
  /** Add an image (data URL + small thumbnail) to the server-side rotation pool. */
888
965
  async function rotationAdd(dataUrl, thumb) {
889
966
  if (!rpcCallFn) return {
@@ -934,7 +1011,7 @@ window.__ModuleLoader__.load({
934
1011
  }
935
1012
  }
936
1013
  /** Activate a rotation item: the server copies its bytes into the wallpaper
937
- * slot and returns the freshly sniffed data URL for immediate display. */
1014
+ * slot and returns the serve URL for immediate display. */
938
1015
  async function rotationActivate(index) {
939
1016
  if (!rpcCallFn) return {
940
1017
  ok: false,
@@ -958,6 +1035,88 @@ window.__ModuleLoader__.load({
958
1035
  }
959
1036
  }
960
1037
  //#endregion
1038
+ //#region src/client/utils/image.ts
1039
+ /**
1040
+ * Read a chosen image file as a data URL WITHOUT re-encoding: the original
1041
+ * pixels are kept as-is (no canvas downscale / JPEG re-compression), so the
1042
+ * wallpaper is stored and displayed at full fidelity. The tradeoff is a larger
1043
+ * payload over the RPC channel and on disk for big images.
1044
+ */
1045
+ function readImg(file, cb) {
1046
+ const r = new FileReader();
1047
+ r.onerror = () => cb(null);
1048
+ r.onload = () => cb(r.result);
1049
+ r.readAsDataURL(file);
1050
+ }
1051
+ /** Promised readImg variant for async flows. */
1052
+ function readImgAsync(file) {
1053
+ return new Promise((resolve) => readImg(file, resolve));
1054
+ }
1055
+ /** Blob → data URL (used when a local asset must be embedded, e.g. theme export). */
1056
+ function blobToDataUrl(blob) {
1057
+ return new Promise((resolve, reject) => {
1058
+ const fr = new FileReader();
1059
+ fr.onload = () => resolve(fr.result);
1060
+ fr.onerror = () => reject(fr.error);
1061
+ fr.readAsDataURL(blob);
1062
+ });
1063
+ }
1064
+ const IMG_CACHE_MAX = 6;
1065
+ const imgCache = /* @__PURE__ */ new Map();
1066
+ /** Decode an image URL once and share the element across callers (LRU-capped). */
1067
+ function loadImage(url) {
1068
+ const hit = imgCache.get(url);
1069
+ if (hit) {
1070
+ imgCache.delete(url);
1071
+ imgCache.set(url, hit);
1072
+ return hit;
1073
+ }
1074
+ const p = new Promise((resolve) => {
1075
+ const img = new Image();
1076
+ img.onerror = () => resolve(null);
1077
+ img.onload = () => resolve(img);
1078
+ img.src = url;
1079
+ });
1080
+ imgCache.set(url, p);
1081
+ while (imgCache.size > IMG_CACHE_MAX) {
1082
+ const oldest = imgCache.keys().next().value;
1083
+ if (oldest !== void 0) imgCache.delete(oldest);
1084
+ }
1085
+ return p;
1086
+ }
1087
+ /** Build a small JPEG thumbnail (rotation-pool picker preview). Resolves null
1088
+ * when the image cannot be decoded. */
1089
+ function makeThumb(dataUrl, maxSide = 96) {
1090
+ return new Promise((resolve) => {
1091
+ const img = new Image();
1092
+ img.onerror = () => resolve(null);
1093
+ img.onload = () => {
1094
+ try {
1095
+ const iw = img.naturalWidth || img.width;
1096
+ const ih = img.naturalHeight || img.height;
1097
+ if (iw <= 0 || ih <= 0) {
1098
+ resolve(null);
1099
+ return;
1100
+ }
1101
+ const k = Math.min(1, maxSide / Math.max(iw, ih));
1102
+ const c = document.createElement("canvas");
1103
+ c.width = Math.max(1, Math.round(iw * k));
1104
+ c.height = Math.max(1, Math.round(ih * k));
1105
+ const g = c.getContext("2d");
1106
+ if (!g) {
1107
+ resolve(null);
1108
+ return;
1109
+ }
1110
+ g.drawImage(img, 0, 0, c.width, c.height);
1111
+ resolve(c.toDataURL("image/jpeg", .72));
1112
+ } catch {
1113
+ resolve(null);
1114
+ }
1115
+ };
1116
+ img.src = dataUrl;
1117
+ });
1118
+ }
1119
+ //#endregion
961
1120
  //#region src/client/utils/color.ts
962
1121
  function hsvToHsl(h, s, v) {
963
1122
  const l = v * (1 - s / 2);
@@ -1164,124 +1323,119 @@ window.__ModuleLoader__.load({
1164
1323
  return rgbToHsl(b.r / b.count, b.g / b.count, b.b / b.count);
1165
1324
  }
1166
1325
  function extractWallpaperPalette(dataUrl, bgState) {
1167
- return new Promise((resolve) => {
1168
- const img = new Image();
1169
- img.onerror = () => resolve(null);
1170
- img.onload = () => {
1171
- try {
1172
- const iw = img.naturalWidth || img.width;
1173
- const ih = img.naturalHeight || img.height;
1174
- const bg = bgState;
1175
- let sx = 0, sy = 0, sw = iw, sh = ih;
1176
- if (bg.iw === iw && bg.ih === ih && bg.iw > 0) {
1177
- const fit = Math.min(window.innerWidth / iw, window.innerHeight / ih);
1178
- const w = iw * fit * bg.zoom;
1179
- const h = ih * fit * bg.zoom;
1180
- sx = Math.max(0, bg.x * window.innerWidth - w / 2);
1181
- sy = Math.max(0, bg.y * window.innerHeight - h / 2);
1182
- sw = Math.min(iw - sx, w);
1183
- sh = Math.min(ih - sy, h);
1184
- if (sw <= 0 || sh <= 0) {
1185
- sx = 0;
1186
- sy = 0;
1187
- sw = iw;
1188
- sh = ih;
1189
- }
1190
- }
1191
- const c = document.createElement("canvas");
1192
- c.width = EXTRACT_SIDE;
1193
- c.height = EXTRACT_SIDE;
1194
- const g = c.getContext("2d", { willReadFrequently: true });
1195
- g.drawImage(img, sx, sy, sw, sh, 0, 0, EXTRACT_SIDE, EXTRACT_SIDE);
1196
- const px = g.getImageData(0, 0, EXTRACT_SIDE, EXTRACT_SIDE).data;
1197
- const counts = /* @__PURE__ */ new Uint32Array(4096);
1198
- const sums = /* @__PURE__ */ new Float64Array(12288);
1199
- let totalLum = 0;
1200
- let sampled = 0;
1201
- for (let i = 0; i < px.length; i += 4) {
1202
- const r = px[i], gg = px[i + 1], b = px[i + 2];
1203
- const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
1204
- const v = max / 255;
1205
- const s = max === 0 ? 0 : (max - min) / max;
1206
- totalLum += (.2126 * r + .7152 * gg + .0722 * b) / 255;
1207
- sampled++;
1208
- if (s < .08 || v < .12 || v > .97) continue;
1209
- const key = r >> 4 << 8 | gg >> 4 << 4 | b >> 4;
1210
- counts[key]++;
1211
- sums[key * 3] += r;
1212
- sums[key * 3 + 1] += gg;
1213
- sums[key * 3 + 2] += b;
1214
- }
1215
- const buckets = [];
1216
- for (let k = 0; k < 4096; k++) {
1217
- if (counts[k] === 0) continue;
1218
- buckets.push({
1219
- count: counts[k],
1220
- r: sums[k * 3],
1221
- g: sums[k * 3 + 1],
1222
- b: sums[k * 3 + 2],
1223
- s: (Math.max(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2]) - Math.min(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2])) / Math.max(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2]) || 0
1224
- });
1225
- }
1226
- if (buckets.length === 0) {
1227
- resolve(null);
1228
- return;
1326
+ return loadImage(dataUrl).then((img) => {
1327
+ if (!img) return null;
1328
+ try {
1329
+ const iw = img.naturalWidth || img.width;
1330
+ const ih = img.naturalHeight || img.height;
1331
+ const bg = bgState;
1332
+ let sx = 0, sy = 0, sw = iw, sh = ih;
1333
+ if (bg.iw === iw && bg.ih === ih && bg.iw > 0) {
1334
+ const fit = Math.min(window.innerWidth / iw, window.innerHeight / ih);
1335
+ const w = iw * fit * bg.zoom;
1336
+ const h = ih * fit * bg.zoom;
1337
+ const px = bg.x * window.innerWidth - w / 2;
1338
+ const py = bg.y * window.innerHeight - h / 2;
1339
+ sx = Math.max(0, -px);
1340
+ sy = Math.max(0, -py);
1341
+ sw = Math.min(iw - sx, window.innerWidth - px - sx);
1342
+ sh = Math.min(ih - sy, window.innerHeight - py - sy);
1343
+ if (sw <= 0 || sh <= 0) {
1344
+ sx = 0;
1345
+ sy = 0;
1346
+ sw = iw;
1347
+ sh = ih;
1229
1348
  }
1230
- buckets.sort((a, b) => b.count * (.5 + b.s) - a.count * (.5 + a.s));
1231
- const primary = bucketsToHsl(buckets[0]);
1232
- const primaryHue = primary[0];
1233
- let secondary = primary;
1234
- for (const b of buckets.slice(1)) {
1235
- const [h] = bucketsToHsl(b);
1236
- if (Math.abs((h - primaryHue + 540) % 360 - 180) > 30) {
1237
- secondary = bucketsToHsl(b);
1238
- break;
1239
- }
1349
+ }
1350
+ const c = document.createElement("canvas");
1351
+ c.width = EXTRACT_SIDE;
1352
+ c.height = EXTRACT_SIDE;
1353
+ const g = c.getContext("2d", { willReadFrequently: true });
1354
+ g.drawImage(img, sx, sy, sw, sh, 0, 0, EXTRACT_SIDE, EXTRACT_SIDE);
1355
+ const px = g.getImageData(0, 0, EXTRACT_SIDE, EXTRACT_SIDE).data;
1356
+ const counts = /* @__PURE__ */ new Uint32Array(4096);
1357
+ const sums = /* @__PURE__ */ new Float64Array(12288);
1358
+ let totalLum = 0;
1359
+ let sampled = 0;
1360
+ for (let i = 0; i < px.length; i += 4) {
1361
+ const r = px[i], gg = px[i + 1], b = px[i + 2];
1362
+ const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
1363
+ const v = max / 255;
1364
+ const s = max === 0 ? 0 : (max - min) / max;
1365
+ totalLum += (.2126 * r + .7152 * gg + .0722 * b) / 255;
1366
+ sampled++;
1367
+ if (s < .08 || v < .12 || v > .97) continue;
1368
+ const key = r >> 4 << 8 | gg >> 4 << 4 | b >> 4;
1369
+ counts[key]++;
1370
+ sums[key * 3] += r;
1371
+ sums[key * 3 + 1] += gg;
1372
+ sums[key * 3 + 2] += b;
1373
+ }
1374
+ const buckets = [];
1375
+ for (let k = 0; k < 4096; k++) {
1376
+ if (counts[k] === 0) continue;
1377
+ buckets.push({
1378
+ count: counts[k],
1379
+ r: sums[k * 3],
1380
+ g: sums[k * 3 + 1],
1381
+ b: sums[k * 3 + 2],
1382
+ s: (Math.max(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2]) - Math.min(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2])) / Math.max(sums[k * 3], sums[k * 3 + 1], sums[k * 3 + 2]) || 0
1383
+ });
1384
+ }
1385
+ if (buckets.length === 0) return null;
1386
+ buckets.sort((a, b) => b.count * (.5 + b.s) - a.count * (.5 + a.s));
1387
+ const primary = bucketsToHsl(buckets[0]);
1388
+ const primaryHue = primary[0];
1389
+ let secondary = primary;
1390
+ for (const b of buckets.slice(1)) {
1391
+ const [h] = bucketsToHsl(b);
1392
+ if (Math.abs((h - primaryHue + 540) % 360 - 180) > 30) {
1393
+ secondary = bucketsToHsl(b);
1394
+ break;
1240
1395
  }
1241
- let tertiary = [
1242
- (primaryHue + 180) % 360,
1243
- Math.min(.7, primary[1]),
1244
- Math.min(.7, primary[2])
1245
- ];
1246
- for (const b of buckets.slice(1)) {
1247
- const [h] = bucketsToHsl(b);
1248
- const sep = Math.abs((h - primaryHue + 540) % 360 - 180);
1249
- if (sep > 90 && sep < 150) {
1250
- tertiary = bucketsToHsl(b);
1251
- break;
1252
- }
1396
+ }
1397
+ let tertiary = [
1398
+ (primaryHue + 180) % 360,
1399
+ Math.min(.7, primary[1]),
1400
+ Math.min(.7, primary[2])
1401
+ ];
1402
+ for (const b of buckets.slice(1)) {
1403
+ const [h] = bucketsToHsl(b);
1404
+ const sep = Math.abs((h - primaryHue + 540) % 360 - 180);
1405
+ if (sep > 90 && sep < 150) {
1406
+ tertiary = bucketsToHsl(b);
1407
+ break;
1253
1408
  }
1254
- const surface = [
1255
- primaryHue,
1256
- Math.min(.06, primary[1] * .3),
1257
- primary[2]
1258
- ];
1259
- const luminance = sampled > 0 ? totalLum / sampled : primary[2];
1260
- primary[2] = luminance < .5 ? Math.min(.44, Math.max(.2, primary[2])) : Math.max(.6, Math.min(.82, primary[2]));
1261
- resolve({
1262
- primary: [
1263
- primary[0],
1264
- Math.min(.9, Math.max(.15, primary[1])),
1265
- primary[2]
1266
- ],
1267
- secondary: [
1268
- secondary[0],
1269
- Math.min(.85, Math.max(.2, secondary[1])),
1270
- Math.min(.75, Math.max(.35, secondary[2]))
1271
- ],
1272
- tertiary: [
1273
- tertiary[0],
1274
- Math.min(.8, Math.max(.2, tertiary[1])),
1275
- Math.min(.7, Math.max(.35, tertiary[2]))
1276
- ],
1277
- surface,
1278
- luminance
1279
- });
1280
- } catch {
1281
- resolve(null);
1282
1409
  }
1283
- };
1284
- img.src = dataUrl;
1410
+ const surface = [
1411
+ primaryHue,
1412
+ Math.min(.06, primary[1] * .3),
1413
+ primary[2]
1414
+ ];
1415
+ const luminance = sampled > 0 ? totalLum / sampled : primary[2];
1416
+ primary[2] = luminance < .5 ? Math.min(.44, Math.max(.2, primary[2])) : Math.max(.6, Math.min(.82, primary[2]));
1417
+ return {
1418
+ primary: [
1419
+ primary[0],
1420
+ Math.min(.9, Math.max(.15, primary[1])),
1421
+ primary[2]
1422
+ ],
1423
+ secondary: [
1424
+ secondary[0],
1425
+ Math.min(.85, Math.max(.2, secondary[1])),
1426
+ Math.min(.75, Math.max(.35, secondary[2]))
1427
+ ],
1428
+ tertiary: [
1429
+ tertiary[0],
1430
+ Math.min(.8, Math.max(.2, tertiary[1])),
1431
+ Math.min(.7, Math.max(.35, tertiary[2]))
1432
+ ],
1433
+ surface,
1434
+ luminance
1435
+ };
1436
+ } catch {
1437
+ return null;
1438
+ }
1285
1439
  });
1286
1440
  }
1287
1441
  /** Backward-compatible single-color extraction: returns the primary HSL. */
@@ -1294,26 +1448,22 @@ window.__ModuleLoader__.load({
1294
1448
  * reads dark (use white fonts), false when light (use black fonts), or null
1295
1449
  * when the frame cannot be decoded. */
1296
1450
  function analyzeFrameDark(dataUrl) {
1297
- return new Promise((resolve) => {
1298
- const img = new Image();
1299
- img.onerror = () => resolve(null);
1300
- img.onload = () => {
1301
- try {
1302
- const c = document.createElement("canvas");
1303
- c.width = ANALYZE_SIDE;
1304
- c.height = ANALYZE_SIDE;
1305
- const g = c.getContext("2d", { willReadFrequently: true });
1306
- g.drawImage(img, 0, 0, ANALYZE_SIDE, ANALYZE_SIDE);
1307
- const px = g.getImageData(0, 0, ANALYZE_SIDE, ANALYZE_SIDE).data;
1308
- let lum = 0;
1309
- const count = px.length / 4;
1310
- for (let i = 0; i < px.length; i += 4) lum += .2126 * px[i] + .7152 * px[i + 1] + .0722 * px[i + 2];
1311
- resolve(lum / count / 255 < .5);
1312
- } catch {
1313
- resolve(null);
1314
- }
1315
- };
1316
- img.src = dataUrl;
1451
+ return loadImage(dataUrl).then((img) => {
1452
+ if (!img) return null;
1453
+ try {
1454
+ const c = document.createElement("canvas");
1455
+ c.width = ANALYZE_SIDE;
1456
+ c.height = ANALYZE_SIDE;
1457
+ const g = c.getContext("2d", { willReadFrequently: true });
1458
+ g.drawImage(img, 0, 0, ANALYZE_SIDE, ANALYZE_SIDE);
1459
+ const px = g.getImageData(0, 0, ANALYZE_SIDE, ANALYZE_SIDE).data;
1460
+ let lum = 0;
1461
+ const count = px.length / 4;
1462
+ for (let i = 0; i < px.length; i += 4) lum += .2126 * px[i] + .7152 * px[i + 1] + .0722 * px[i + 2];
1463
+ return lum / count / 255 < .5;
1464
+ } catch {
1465
+ return null;
1466
+ }
1317
1467
  });
1318
1468
  }
1319
1469
  /** HSL (h 0-360, s/l 0-1) → RGB (0-255 integers). */
@@ -1355,20 +1505,42 @@ window.__ModuleLoader__.load({
1355
1505
  function newRaf(canvas, draw) {
1356
1506
  canvas.dataset.dshAnyCanvas = "1";
1357
1507
  let running = true;
1508
+ let paused = false;
1509
+ let rafId = 0;
1358
1510
  let last = 0;
1359
1511
  draw();
1512
+ if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return {
1513
+ stop: () => {
1514
+ running = false;
1515
+ },
1516
+ pause: () => void 0,
1517
+ resume: () => void 0
1518
+ };
1360
1519
  const loop = (ts) => {
1361
- if (!running) return;
1520
+ if (!running || paused) return;
1362
1521
  if (ts - last >= FRAME_MS) {
1363
1522
  last = ts;
1364
1523
  draw();
1365
1524
  }
1366
- requestAnimationFrame(loop);
1525
+ rafId = requestAnimationFrame(loop);
1526
+ };
1527
+ rafId = requestAnimationFrame(loop);
1528
+ return {
1529
+ stop: () => {
1530
+ running = false;
1531
+ cancelAnimationFrame(rafId);
1532
+ },
1533
+ pause: () => {
1534
+ paused = true;
1535
+ cancelAnimationFrame(rafId);
1536
+ },
1537
+ resume: () => {
1538
+ if (!running || !paused) return;
1539
+ paused = false;
1540
+ last = 0;
1541
+ rafId = requestAnimationFrame(loop);
1542
+ }
1367
1543
  };
1368
- requestAnimationFrame(loop);
1369
- return { stop: () => {
1370
- running = false;
1371
- } };
1372
1544
  }
1373
1545
  function createCanvas() {
1374
1546
  const c = document.createElement("canvas");
@@ -1383,7 +1555,6 @@ window.__ModuleLoader__.load({
1383
1555
  };
1384
1556
  }
1385
1557
  function fitLiveCanvas(c) {
1386
- if (c.dataset.dshAnyStatic === "1") return;
1387
1558
  const { w, h } = liveSize();
1388
1559
  if (c.width !== w || c.height !== h) {
1389
1560
  c.width = w;
@@ -1680,6 +1851,43 @@ window.__ModuleLoader__.load({
1680
1851
  col = hueRotate(col, u_seed * 6.28318530718);
1681
1852
  gl_FragColor = vec4(col, 1.0);
1682
1853
  }
1854
+ `;
1855
+ if (preset === "starfield") return common + `
1856
+ float hash21(vec2 p) {
1857
+ p = fract(p * vec2(234.34, 435.345));
1858
+ p += dot(p, p + 34.23);
1859
+ return fract(p.x * p.y);
1860
+ }
1861
+ vec3 starLayer(vec2 uv, float t, float density) {
1862
+ vec3 col = vec3(0.0);
1863
+ vec2 grid = uv * density;
1864
+ vec2 cell = floor(grid);
1865
+ vec2 f = fract(grid) - 0.5;
1866
+ float h = hash21(cell);
1867
+ if (h > 0.8) {
1868
+ vec2 offs = (vec2(hash21(cell + 1.3), hash21(cell + 2.7)) - 0.5) * 0.7;
1869
+ float d = length(f - offs);
1870
+ float tw = 0.35 + 0.65 * sin(t * (1.0 + h * 2.5) + h * 40.0);
1871
+ col += vec3(0.85, 0.92, 1.0) * smoothstep(0.07, 0.0, d) * max(0.0, tw);
1872
+ }
1873
+ return col;
1874
+ }
1875
+ void main() {
1876
+ vec2 uv = gl_FragCoord.xy / u_resolution;
1877
+ vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;
1878
+ float t = u_time * 0.5;
1879
+ vec3 col = mix(vec3(0.012, 0.014, 0.035), vec3(0.035, 0.035, 0.08), uv.y);
1880
+ // Faint drifting nebula veil behind the stars.
1881
+ float n = fbm(vec3(p * 2.2 * u_scale, t * 0.05));
1882
+ col += vec3(0.05, 0.045, 0.11) * smoothstep(0.05, 0.85, n);
1883
+ vec3 stars = vec3(0.0);
1884
+ stars += starLayer(p + vec2(t * 0.006, 0.0), t, 13.0 * u_scale);
1885
+ stars += starLayer(p * 1.8 + vec2(t * 0.013, 3.0), t * 1.25, 27.0 * u_scale);
1886
+ stars += starLayer(p * 3.1 + vec2(t * 0.021, 7.0), t * 0.85, 46.0 * u_scale);
1887
+ col += stars;
1888
+ col = hueRotate(col, u_seed * 6.28318530718);
1889
+ gl_FragColor = vec4(col, 1.0);
1890
+ }
1683
1891
  `;
1684
1892
  return common + `
1685
1893
  void main() {
@@ -1703,8 +1911,154 @@ window.__ModuleLoader__.load({
1703
1911
  function createPatternBg(params, canvas) {
1704
1912
  if (params.preset === "waves") return createWaves(params, canvas);
1705
1913
  if (params.preset === "poly") return createLowPoly(params, canvas);
1914
+ if (params.preset === "rain") return createRain(params, canvas);
1915
+ if (params.preset === "contour") return createContour(params, canvas);
1916
+ if (params.preset === "meta") return createMeta(params, canvas);
1706
1917
  return createDots(params, canvas);
1707
1918
  }
1919
+ function createRain(params, canvas) {
1920
+ const c = canvas ?? createCanvas();
1921
+ fitLiveCanvas(c);
1922
+ const g = c.getContext("2d", { alpha: false });
1923
+ const rng = createRng(params.seed);
1924
+ const dark = rng() < .5;
1925
+ const hue = Math.round(rng() * 360);
1926
+ const count = Math.round(30 + params.density * 150);
1927
+ const drops = [];
1928
+ for (let i = 0; i < count; i++) drops.push({
1929
+ x: rng(),
1930
+ y: rng(),
1931
+ len: (.035 + rng() * .075) * (.7 + params.scale * .5),
1932
+ speed: (.25 + rng() * .55) * (.6 + params.scale * .5),
1933
+ sat: Math.round(30 + rng() * 40),
1934
+ lit: dark ? Math.round(55 + rng() * 25) : Math.round(40 + rng() * 25),
1935
+ alpha: .12 + rng() * .28,
1936
+ width: .8 + rng() * 1.4
1937
+ });
1938
+ let t = 0;
1939
+ const draw = () => {
1940
+ fitLiveCanvas(c);
1941
+ const w = c.width, h = c.height;
1942
+ g.fillStyle = dark ? "#07080c" : "#f4f6f9";
1943
+ g.fillRect(0, 0, w, h);
1944
+ for (const d of drops) {
1945
+ const yy = ((d.y + t * d.speed) % 1 + 1) % 1;
1946
+ const x = d.x * w;
1947
+ const y0 = yy * h;
1948
+ const y1 = y0 - d.len * h;
1949
+ const grad = g.createLinearGradient(x, y0, x, y1);
1950
+ grad.addColorStop(0, `hsla(${hue},${d.sat}%,${d.lit}%,${d.alpha.toFixed(2)})`);
1951
+ grad.addColorStop(1, "hsla(0,0%,0%,0)");
1952
+ g.strokeStyle = grad;
1953
+ g.lineWidth = d.width;
1954
+ g.beginPath();
1955
+ g.moveTo(x, y0);
1956
+ g.lineTo(x, y1);
1957
+ g.stroke();
1958
+ }
1959
+ t += .016;
1960
+ };
1961
+ return {
1962
+ canvas: c,
1963
+ ...newRaf(c, draw),
1964
+ snapshot: () => c.toDataURL("image/jpeg", .94)
1965
+ };
1966
+ }
1967
+ function createContour(params, canvas) {
1968
+ const c = canvas ?? createCanvas();
1969
+ fitLiveCanvas(c);
1970
+ const g = c.getContext("2d", { alpha: false });
1971
+ const noise = createNoise(params.seed);
1972
+ const rng = createRng(params.seed + 1013);
1973
+ const dark = rng() < .5;
1974
+ const hue = Math.round(rng() * 360);
1975
+ const layers = Math.round(16 + params.density * 46);
1976
+ const freq = .0016 / params.scale;
1977
+ const amp = .055 * params.scale;
1978
+ let t = 0;
1979
+ const draw = () => {
1980
+ fitLiveCanvas(c);
1981
+ const w = c.width, h = c.height;
1982
+ g.fillStyle = dark ? "#0a0b0e" : "#f6f7fa";
1983
+ g.fillRect(0, 0, w, h);
1984
+ const samples = Math.max(60, Math.floor(w / 8));
1985
+ for (let i = 0; i <= layers; i++) {
1986
+ const base = i / layers;
1987
+ const shift = t * .05;
1988
+ g.beginPath();
1989
+ for (let s = 0; s <= samples; s++) {
1990
+ const fx = s / samples;
1991
+ const y = (base + (noise(fx * 60 * freq * 400, i * .22 + shift) - .5) * amp) * h;
1992
+ if (s === 0) g.moveTo(0, y);
1993
+ else g.lineTo(fx * w, y);
1994
+ }
1995
+ const lit = dark ? 20 + i / layers * 22 : 62 - i / layers * 18;
1996
+ g.strokeStyle = `hsla(${(hue + i * 2) % 360},30%,${lit}%,${(.25 + .2 * (i / layers)).toFixed(2)})`;
1997
+ g.lineWidth = 1;
1998
+ g.stroke();
1999
+ }
2000
+ t += .032;
2001
+ };
2002
+ return {
2003
+ canvas: c,
2004
+ ...newRaf(c, draw),
2005
+ snapshot: () => c.toDataURL("image/jpeg", .94)
2006
+ };
2007
+ }
2008
+ function createMeta(params, canvas) {
2009
+ const c = canvas ?? createCanvas();
2010
+ fitLiveCanvas(c);
2011
+ const g = c.getContext("2d", { alpha: false });
2012
+ const rng = createRng(params.seed);
2013
+ const dark = rng() < .5;
2014
+ const baseHue = Math.round(rng() * 360);
2015
+ const count = Math.round(5 + params.density * 11);
2016
+ const blobs = [];
2017
+ for (let i = 0; i < count; i++) blobs.push({
2018
+ x: .15 + rng() * .7,
2019
+ y: .15 + rng() * .7,
2020
+ r: (.09 + rng() * .14) * params.scale,
2021
+ ax: .08 + rng() * .22,
2022
+ ay: .08 + rng() * .22,
2023
+ speed: .15 + rng() * .3,
2024
+ phase: rng() * Math.PI * 2,
2025
+ hue: (baseHue + rng() * 70) % 360
2026
+ });
2027
+ let t = 0;
2028
+ const draw = () => {
2029
+ fitLiveCanvas(c);
2030
+ const w = c.width, h = c.height;
2031
+ g.fillStyle = dark ? "#08090d" : "#f5f6f9";
2032
+ g.fillRect(0, 0, w, h);
2033
+ g.globalCompositeOperation = dark ? "lighter" : "multiply";
2034
+ for (const b of blobs) {
2035
+ const cx = (b.x + Math.sin(t * b.speed + b.phase) * b.ax) * w;
2036
+ const cy = (b.y + Math.cos(t * b.speed * .83 + b.phase * 1.7) * b.ay) * h;
2037
+ const r = b.r * Math.min(w, h) * (.9 + .2 * Math.sin(t * b.speed + b.phase));
2038
+ const rad = g.createRadialGradient(cx, cy, 0, cx, cy, r);
2039
+ if (dark) {
2040
+ rad.addColorStop(0, `hsla(${b.hue},65%,58%,0.55)`);
2041
+ rad.addColorStop(.7, `hsla(${b.hue},60%,45%,0.18)`);
2042
+ rad.addColorStop(1, "hsla(0,0%,0%,0)");
2043
+ } else {
2044
+ rad.addColorStop(0, `hsla(${b.hue},55%,70%,0.5)`);
2045
+ rad.addColorStop(.7, `hsla(${b.hue},50%,80%,0.2)`);
2046
+ rad.addColorStop(1, "hsla(0,0%,100%,0)");
2047
+ }
2048
+ g.fillStyle = rad;
2049
+ g.beginPath();
2050
+ g.arc(cx, cy, r, 0, Math.PI * 2);
2051
+ g.fill();
2052
+ }
2053
+ g.globalCompositeOperation = "source-over";
2054
+ t += .03;
2055
+ };
2056
+ return {
2057
+ canvas: c,
2058
+ ...newRaf(c, draw),
2059
+ snapshot: () => c.toDataURL("image/jpeg", .94)
2060
+ };
2061
+ }
1708
2062
  function createDots(params, canvas) {
1709
2063
  const c = canvas ?? createCanvas();
1710
2064
  fitLiveCanvas(c);
@@ -1909,6 +2263,14 @@ window.__ModuleLoader__.load({
1909
2263
  let wpController = null;
1910
2264
  let snapshotListener = null;
1911
2265
  let tokenStyleEl = null;
2266
+ /** Pause the live generated background's animation loop (session-only). */
2267
+ function pauseGeneratedBg() {
2268
+ wpController?.pause?.();
2269
+ }
2270
+ /** Resume the live generated background's animation loop. */
2271
+ function resumeGeneratedBg() {
2272
+ wpController?.resume?.();
2273
+ }
1912
2274
  function clearDynamicBg() {
1913
2275
  wpController?.stop();
1914
2276
  wpController?.canvas.remove();
@@ -2061,6 +2423,14 @@ window.__ModuleLoader__.load({
2061
2423
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-input-blur", `blur(${px}px)`);
2062
2424
  else document.documentElement.style.removeProperty("--dsh-any-input-blur");
2063
2425
  }
2426
+ const MOBILE_HEADER_RULE = ".dsh-mobile-app-header{background:var(--dsh-any-op-bg,transparent)!important;backdrop-filter:var(--dsh-any-part-blur-global,none);-webkit-backdrop-filter:var(--dsh-any-part-blur-global,none)}";
2427
+ /** Mirror of the bg-part blur on :root. --dsh-any-part-blur is element-scoped
2428
+ * to the columns, so surfaces outside their subtree (the mobile header, or
2429
+ * third-party styles) can never inherit it. */
2430
+ function applyBgBlurGlobal(px) {
2431
+ if (px > 0) document.documentElement.style.setProperty("--dsh-any-part-blur-global", `blur(${px}px)`);
2432
+ else document.documentElement.style.removeProperty("--dsh-any-part-blur-global");
2433
+ }
2064
2434
  function applySettingsOverrides(op) {
2065
2435
  const [h, s, l] = rColor();
2066
2436
  const tokens = genTokens(h, s, l, rColorScheme()).tokens;
@@ -2099,14 +2469,16 @@ window.__ModuleLoader__.load({
2099
2469
  return;
2100
2470
  }
2101
2471
  const url = rWp();
2102
- if (url) extractWallpaperColor(url, cfg.backgroundType === "video" ? rVideoBgState() : rBgState()).then((hsl) => {
2103
- if (hsl) {
2104
- cfg.color = hsl;
2105
- colorAdoptedListener?.(hsl);
2106
- }
2472
+ if (url) {
2107
2473
  applyWp();
2108
- });
2109
- else applyWp();
2474
+ extractWallpaperColor(url, cfg.backgroundType === "video" ? rVideoBgState() : rBgState()).then((hsl) => {
2475
+ if (hsl && rWp() === url) {
2476
+ cfg.color = hsl;
2477
+ colorAdoptedListener?.(hsl);
2478
+ }
2479
+ applyWp();
2480
+ });
2481
+ } else applyWp();
2110
2482
  }
2111
2483
  /** Switch the background source type. For generated types a new live canvas is
2112
2484
  * attached to the wallpaper layer and a snapshot is kept for the store/preview. */
@@ -2258,6 +2630,7 @@ window.__ModuleLoader__.load({
2258
2630
  setBlur(sidebarEl, blurs.sidebar);
2259
2631
  setBlur(centerEl, blurs.bg);
2260
2632
  setBlur(detailsEl, blurs.bg);
2633
+ applyBgBlurGlobal(blurs.bg);
2261
2634
  applyCardPanelsBlur(blurs.card);
2262
2635
  applySettingsBlur(blurs.settings);
2263
2636
  applyInputBlur(blurs.input);
@@ -2285,6 +2658,7 @@ window.__ModuleLoader__.load({
2285
2658
  if (part === "bg") {
2286
2659
  setBlur(centerEl, v);
2287
2660
  setBlur(detailsEl, v);
2661
+ applyBgBlurGlobal(v);
2288
2662
  } else setBlur(sidebarEl, v);
2289
2663
  }
2290
2664
  const VIEW_CARDS = [{
@@ -2554,17 +2928,18 @@ window.__ModuleLoader__.load({
2554
2928
  cb(imgNat.w, imgNat.h);
2555
2929
  return;
2556
2930
  }
2557
- const img = new Image();
2558
- img.onload = () => {
2931
+ loadImage(url).then((img) => {
2932
+ if (!img) {
2933
+ cb(0, 0);
2934
+ return;
2935
+ }
2559
2936
  imgNat = {
2560
2937
  url,
2561
2938
  w: img.naturalWidth,
2562
2939
  h: img.naturalHeight
2563
2940
  };
2564
2941
  cb(img.naturalWidth, img.naturalHeight);
2565
- };
2566
- img.onerror = () => cb(0, 0);
2567
- img.src = url;
2942
+ });
2568
2943
  }
2569
2944
  const DRAG_MAX_SIDE = 720;
2570
2945
  let lowResUrl = null;
@@ -2575,8 +2950,11 @@ window.__ModuleLoader__.load({
2575
2950
  cb(lowResUrl);
2576
2951
  return;
2577
2952
  }
2578
- const img = new Image();
2579
- img.onload = () => {
2953
+ loadImage(url).then((img) => {
2954
+ if (!img) {
2955
+ cb(null);
2956
+ return;
2957
+ }
2580
2958
  const k = Math.min(1, DRAG_MAX_SIDE / Math.max(img.naturalWidth, img.naturalHeight));
2581
2959
  if (k >= 1) {
2582
2960
  lowResFor = url;
@@ -2599,9 +2977,7 @@ window.__ModuleLoader__.load({
2599
2977
  lowResFor = url;
2600
2978
  lowResUrl = low;
2601
2979
  cb(low);
2602
- };
2603
- img.onerror = () => cb(null);
2604
- img.src = url;
2980
+ });
2605
2981
  }
2606
2982
  function setDragLow(on) {
2607
2983
  if (cfg.backgroundType !== "image" || on === dragLow || !wpEl) return;
@@ -2640,6 +3016,7 @@ window.__ModuleLoader__.load({
2640
3016
  }
2641
3017
  let wpVerdict = null;
2642
3018
  let verdictListener = null;
3019
+ let verdictGen = 0;
2643
3020
  /** Register a callback fired when the background brightness verdict CHANGES
2644
3021
  * (a new wallpaper was analyzed, a generated bg regenerated), so the skin can
2645
3022
  * be re-registered through the host theme service. */
@@ -2652,6 +3029,7 @@ window.__ModuleLoader__.load({
2652
3029
  if (dark !== null) verdictListener?.();
2653
3030
  }
2654
3031
  function updateWpVerdict(url) {
3032
+ const gen = ++verdictGen;
2655
3033
  if (url === null) {
2656
3034
  applyVerdict(null);
2657
3035
  return;
@@ -2661,7 +3039,7 @@ window.__ModuleLoader__.load({
2661
3039
  return;
2662
3040
  }
2663
3041
  analyzeFrameDark(url).then((dark) => {
2664
- if (dark === null) return;
3042
+ if (dark === null || gen !== verdictGen) return;
2665
3043
  wpVerdict = {
2666
3044
  url,
2667
3045
  dark
@@ -2791,6 +3169,7 @@ window.__ModuleLoader__.load({
2791
3169
  setBgDark(null);
2792
3170
  }
2793
3171
  if (rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto") applyCustomTokens(rOps());
3172
+ else if (!url) clearThemeTokens();
2794
3173
  if (rHasColor()) {
2795
3174
  applySettingsOverrides(rSop());
2796
3175
  applyTrajectoryOverrides(rTrajectoryOpacity());
@@ -2822,6 +3201,7 @@ window.__ModuleLoader__.load({
2822
3201
  document.documentElement.style.removeProperty("--dsh-any-blur-settings");
2823
3202
  document.documentElement.style.removeProperty("--dsh-any-blur-card-panels");
2824
3203
  document.documentElement.style.removeProperty("--dsh-any-input-blur");
3204
+ document.documentElement.style.removeProperty("--dsh-any-part-blur-global");
2825
3205
  for (const v of Object.values(OPACITY_VARS)) document.documentElement.style.removeProperty(v);
2826
3206
  baseTokenKey = "";
2827
3207
  lastBgKey = "";
@@ -2912,56 +3292,6 @@ window.__ModuleLoader__.load({
2912
3292
  v.src = url;
2913
3293
  });
2914
3294
  }
2915
- //#endregion
2916
- //#region src/client/utils/image.ts
2917
- /**
2918
- * Read a chosen image file as a data URL WITHOUT re-encoding: the original
2919
- * pixels are kept as-is (no canvas downscale / JPEG re-compression), so the
2920
- * wallpaper is stored and displayed at full fidelity. The tradeoff is a larger
2921
- * payload over the RPC channel and on disk for big images.
2922
- */
2923
- function readImg(file, cb) {
2924
- const r = new FileReader();
2925
- r.onerror = () => cb(null);
2926
- r.onload = () => cb(r.result);
2927
- r.readAsDataURL(file);
2928
- }
2929
- /** Promised readImg variant for async flows. */
2930
- function readImgAsync(file) {
2931
- return new Promise((resolve) => readImg(file, resolve));
2932
- }
2933
- /** Build a small JPEG thumbnail (rotation-pool picker preview). Resolves null
2934
- * when the image cannot be decoded. */
2935
- function makeThumb(dataUrl, maxSide = 96) {
2936
- return new Promise((resolve) => {
2937
- const img = new Image();
2938
- img.onerror = () => resolve(null);
2939
- img.onload = () => {
2940
- try {
2941
- const iw = img.naturalWidth || img.width;
2942
- const ih = img.naturalHeight || img.height;
2943
- if (iw <= 0 || ih <= 0) {
2944
- resolve(null);
2945
- return;
2946
- }
2947
- const k = Math.min(1, maxSide / Math.max(iw, ih));
2948
- const c = document.createElement("canvas");
2949
- c.width = Math.max(1, Math.round(iw * k));
2950
- c.height = Math.max(1, Math.round(ih * k));
2951
- const g = c.getContext("2d");
2952
- if (!g) {
2953
- resolve(null);
2954
- return;
2955
- }
2956
- g.drawImage(img, 0, 0, c.width, c.height);
2957
- resolve(c.toDataURL("image/jpeg", .72));
2958
- } catch {
2959
- resolve(null);
2960
- }
2961
- };
2962
- img.src = dataUrl;
2963
- });
2964
- }
2965
3295
  const UI_CSS = `
2966
3296
  /* The section is rendered INLINE inside the host settings dialog's content
2967
3297
  * column: the host provides the modal chrome (backdrop, centering, closing).
@@ -3031,6 +3361,7 @@ window.__ModuleLoader__.load({
3031
3361
  .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}
3032
3362
  .dab-urlinput::placeholder{color:var(--dsw-alias-label-quaternary)}
3033
3363
  .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)}
3364
+ .dab-urlrow{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:12px;animation:dab-list-in .3s cubic-bezier(.22,1,.36,1) both}
3034
3365
  .dab-swatch-lg{height:38px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);box-shadow:inset 0 0 14px rgba(0,0,0,.1);transition:transform .3s ease}
3035
3366
  .dab-swatch-lg:hover{transform:scale(1.02)}
3036
3367
 
@@ -3041,11 +3372,19 @@ window.__ModuleLoader__.load({
3041
3372
  .dab-btn:disabled{opacity:.5;cursor:not-allowed}
3042
3373
  .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);border-color:transparent}
3043
3374
  .dab-btn-danger{color:var(--dsw-alias-state-error-primary)}
3375
+ .dab-btn-danger-solid{background:var(--dsw-alias-state-error-primary);color:#fff;border-color:transparent}
3376
+ .dab-btn-danger-solid:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 85%,#000)}
3044
3377
  .dab-btn-ghost{background:transparent;border-color:transparent;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
3045
3378
  .dab-btn-ghost:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);box-shadow:none}
3379
+ /* Soft solid button: subtle layer background instead of transparent ghost. */
3380
+ .dab-btn-soft{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
3381
+ .dab-btn-soft:hover:not(:disabled){background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary)}
3382
+ /* White background with error-red text (inverse of danger-solid). */
3383
+ .dab-btn-danger-inverted{background:#fff;color:var(--dsw-alias-state-error-primary);border-color:transparent}
3384
+ .dab-btn-danger-inverted:hover:not(:disabled){background:#f3f4f6}
3046
3385
  .dab-btn:focus-visible,.dab-nav-item:focus-visible,.dab-seg-item:focus-visible,.dab-swatch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
3047
3386
  .dab-chip-row{display:flex;flex-wrap:wrap;gap:8px}
3048
- .dab-chip{height:30px;padding:0 14px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .22s ease}
3387
+ .dab-chip{height:30px;padding:0 14px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary));font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:background-color .22s ease,border-color .22s ease,color .22s ease}
3049
3388
  .dab-chip:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
3050
3389
  .dab-chip.is-active{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);border-color:transparent}
3051
3390
 
@@ -3095,7 +3434,7 @@ window.__ModuleLoader__.load({
3095
3434
 
3096
3435
  /* ── generated background type cards ─────────────────────────────────────── */
3097
3436
  .dab-types{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
3098
- .dab-type{position:relative;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:14px;padding:10px;cursor:pointer;text-align:left;font:inherit;transition:border-color .25s,transform .25s,box-shadow .25s}
3437
+ .dab-type{position:relative;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:14px;padding:10px;cursor:pointer;text-align:left;font:inherit;transition:border-color .25s,transform .25s,box-shadow .25s;animation:dab-type-in .42s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--i,0) * 60ms)}
3099
3438
  .dab-type:hover{transform:translateY(-2px)}
3100
3439
  .dab-type.is-active{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
3101
3440
  .dab-type-thumb{height:62px;border-radius:9px;overflow:hidden;position:relative}
@@ -3126,14 +3465,15 @@ window.__ModuleLoader__.load({
3126
3465
  /* ── presets / profiles / schedule / rotation ────────────────────────────── */
3127
3466
  .dab-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
3128
3467
  .dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
3129
- .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}
3468
+ .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)}
3130
3469
  .dab-preset:hover{transform:translateY(-2px);border-color:var(--dsw-alias-brand-primary);box-shadow:0 8px 22px -12px rgba(0,0,0,.3)}
3131
3470
  .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)}
3132
3471
  .dab-preset-name{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary)}
3133
3472
  .dab-preset-desc{font-size:10.5px;line-height:1.45;color:var(--dsw-alias-label-tertiary)}
3134
- .dab-profile-list{display:flex;flex-direction:column;gap:8px;margin-top:12px}
3135
- .dab-profile-row{display:flex;align-items:center;gap:11px;padding:9px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2);transition:border-color .22s ease}
3136
- .dab-profile-row.is-active{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent)}
3473
+ .dab-profile-list{display:flex;flex-direction:column;gap:8px;margin-top:12px;animation:dab-list-in .38s cubic-bezier(.22,1,.36,1) both}
3474
+ .dab-profile-row{display:flex;align-items:center;gap:11px;padding:9px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2);animation:dab-row-in .34s cubic-bezier(.22,1,.36,1) both;animation-delay:calc(var(--i,0) * 45ms);transition:border-color .22s ease,background .22s ease}
3475
+ .dab-profile-row:hover{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 28%,transparent);background:color-mix(in srgb,var(--dsw-alias-label-primary) 4%,var(--dsw-alias-bg-layer-2))}
3476
+ .dab-profile-row.is-active{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 7%,var(--dsw-alias-bg-layer-2))}
3137
3477
  .dab-profile-dot{width:22px;height:22px;flex:none;border-radius:50%;background:var(--dsw-alias-bg-layer-1);box-shadow:inset 0 0 0 1px rgba(0,0,0,.12);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary)}
3138
3478
  .dab-profile-meta{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
3139
3479
  .dab-profile-name{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -3143,14 +3483,22 @@ window.__ModuleLoader__.load({
3143
3483
  .dab-select:focus{border-color:var(--dsw-alias-brand-primary)}
3144
3484
  .dab-timeinput{height:32px;padding:0 8px;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:12px;outline:none}
3145
3485
  .dab-timeinput:focus{border-color:var(--dsw-alias-brand-primary)}
3146
- .dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-top:12px}
3147
- .dab-schedule-cell{min-width:0}
3486
+ /* Collapsible panel: animates grid-template-rows 0fr→1fr so the schedule
3487
+ block unfolds to its content height instead of popping in. visibility
3488
+ flips after the fold finishes (and instantly on open) so hidden inputs
3489
+ are never focusable. */
3490
+ .dab-schedule-wrap{display:grid;grid-template-rows:0fr;visibility:hidden;transition:grid-template-rows .42s cubic-bezier(.22,1,.36,1),visibility 0s linear .42s}
3491
+ .dab-schedule-wrap.is-open{grid-template-rows:1fr;visibility:visible;transition:grid-template-rows .42s cubic-bezier(.22,1,.36,1)}
3492
+ .dab-schedule-clip{overflow:hidden;min-height:0;opacity:0;transition:opacity .28s ease}
3493
+ .dab-schedule-wrap.is-open .dab-schedule-clip{opacity:1;transition:opacity .32s ease .1s}
3494
+ .dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;padding-top:12px}
3495
+ .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)}
3148
3496
  .dab-time-row{display:flex;flex-direction:column;gap:9px}
3149
3497
  .dab-time-label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
3150
3498
  .dab-time-label svg{color:var(--dsw-alias-brand-primary);flex:none}
3151
3499
  .dab-time-label .dab-select,.dab-time-label .dab-timeinput{flex:1;min-width:0}
3152
3500
  .dab-thumbstrip{display:flex;flex-wrap:wrap;gap:9px;margin-top:13px}
3153
- .dab-thumb{position:relative;width:64px;height:44px;border-radius:9px;overflow:hidden;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary)}
3501
+ .dab-thumb{position:relative;width:64px;height:44px;border-radius:9px;overflow:hidden;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary);animation:dab-list-in .3s cubic-bezier(.22,1,.36,1) both}
3154
3502
  .dab-thumb.is-current{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,transparent)}
3155
3503
  .dab-thumb img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
3156
3504
  .dab-thumb-x{position:absolute;top:2px;right:2px;width:16px;height:16px;border:0;border-radius:50%;background:rgba(0,0,0,.55);color:#fff;cursor:pointer;display:grid;place-items:center;opacity:0;transition:opacity .18s ease,background .18s ease;padding:0}
@@ -3184,6 +3532,9 @@ window.__ModuleLoader__.load({
3184
3532
  @keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
3185
3533
  @keyframes dab-page-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
3186
3534
  @keyframes dab-rise-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
3535
+ @keyframes dab-list-in{from{opacity:0;transform:translateY(-7px)}to{opacity:1;transform:none}}
3536
+ @keyframes dab-row-in{from{opacity:0;transform:translateY(6px) scale(.985)}to{opacity:1;transform:none}}
3537
+ @keyframes dab-type-in{from{opacity:0;transform:translateY(8px) scale(.97)}to{opacity:1;transform:none}}
3187
3538
  @keyframes dab-orb-in{from{opacity:0;transform:scale(.82)}to{opacity:1;transform:none}}
3188
3539
  @keyframes dab-spin{to{transform:rotate(360deg)}}
3189
3540
  @keyframes dab-rotate{to{transform:rotate(360deg)}}
@@ -3491,6 +3842,16 @@ window.__ModuleLoader__.load({
3491
3842
  className,
3492
3843
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
3493
3844
  });
3845
+ const PlayIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3846
+ size,
3847
+ className,
3848
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.2 3.4l7.4 4.6-7.4 4.6V3.4z" })
3849
+ });
3850
+ const PauseIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3851
+ size,
3852
+ className,
3853
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.4 3.4v9.2M10.6 3.4v9.2" })
3854
+ });
3494
3855
  //#endregion
3495
3856
  //#region src/client/components/ErrorBoundary.tsx
3496
3857
  /**
@@ -4134,12 +4495,12 @@ window.__ModuleLoader__.load({
4134
4495
  }
4135
4496
  function ColorPage({ p, notify }) {
4136
4497
  const { t, hue, sat, lit, setColor, extractColor, setSchemeOverride, useStore } = p;
4137
- const store = useStore((s) => s);
4138
- const storeUrl = store.url;
4139
- const schemeOverride = store.schemeOverride;
4498
+ const storeUrl = useStore((s) => s.url);
4499
+ const schemeOverride = useStore((s) => s.schemeOverride);
4500
+ const color = useStore((s) => s.color);
4140
4501
  const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
4141
4502
  const [extracting, setExtracting] = (0, react.useState)(false);
4142
- const wheel = store.color ?? [
4503
+ const wheel = color ?? [
4143
4504
  hue,
4144
4505
  sat,
4145
4506
  lit
@@ -4781,13 +5142,12 @@ window.__ModuleLoader__.load({
4781
5142
  }
4782
5143
  ];
4783
5144
  function BackgroundPage({ p }) {
4784
- const { t, setWp, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, setRotation, addRotationItems, removeRotationItem, rotateNow, useStore } = p;
4785
- const store = useStore((s) => s);
4786
- const storeUrl = store.url;
4787
- const backgroundType = store.backgroundType;
4788
- const generatedBg = store.generatedBg;
4789
- const regenerateOnReload = store.regenerateOnReload;
4790
- const rotation = store.rotation;
5145
+ const { t, setWpFromServer, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, setRotation, addRotationItems, removeRotationItem, rotateNow, setVideoFromUrl, useStore } = p;
5146
+ const storeUrl = useStore((s) => s.url);
5147
+ const backgroundType = useStore((s) => s.backgroundType);
5148
+ const generatedBg = useStore((s) => s.generatedBg);
5149
+ const regenerateOnReload = useStore((s) => s.regenerateOnReload);
5150
+ const rotation = useStore((s) => s.rotation);
4791
5151
  const fileRef = (0, react.useRef)(null);
4792
5152
  const rotFileRef = (0, react.useRef)(null);
4793
5153
  const [editorOpen, setEditorOpen] = (0, react.useState)(false);
@@ -4803,13 +5163,22 @@ window.__ModuleLoader__.load({
4803
5163
  const isStatic = backgroundType === "image" || isVideo;
4804
5164
  const isGenerated = !isStatic;
4805
5165
  const activeGenType = isGenerated && generatedBg ? generatedBg.type : "mesh";
5166
+ const [paused, setPaused] = (0, react.useState)(false);
5167
+ const genPreset = generatedBg !== null && generatedBg.type !== "mesh" ? generatedBg.preset : void 0;
5168
+ (0, react.useEffect)(() => {
5169
+ setPaused(false);
5170
+ }, [
5171
+ activeGenType,
5172
+ genPreset,
5173
+ generatedBg?.seed
5174
+ ]);
4806
5175
  const onFileSelect = (f) => {
4807
5176
  if (f.type.startsWith("video/")) {
4808
5177
  setVideo(f, f.type);
4809
5178
  return;
4810
5179
  }
4811
- readImg(f, (d) => {
4812
- if (d) setWp(d);
5180
+ uploadWallpaper(f).then((ok) => {
5181
+ if (ok) setWpFromServer(WALLPAPER_SERVE_URL);
4813
5182
  });
4814
5183
  };
4815
5184
  const onDrop = (e) => {
@@ -4827,12 +5196,17 @@ window.__ModuleLoader__.load({
4827
5196
  setUrlBusy(true);
4828
5197
  setUrlErr(null);
4829
5198
  const r = await setWallpaperFromUrl(u);
4830
- setUrlBusy(false);
4831
5199
  if (r.ok) {
4832
- setWp(r.dataUrl ?? null);
5200
+ setWpFromServer(r.wallpaperUrl ?? null);
4833
5201
  setUrlOpen(false);
4834
5202
  setUrlVal("");
5203
+ } else if (r.error === "not an image") {
5204
+ if (await setVideoFromUrl(u)) {
5205
+ setUrlOpen(false);
5206
+ setUrlVal("");
5207
+ } else setUrlErr(t("bgUrlVideoFail"));
4835
5208
  } else setUrlErr(r.error === "invalid url" || r.error === "unsupported scheme" ? t("bgUrlBadHttp") : r.error ?? t("bgUrlFail"));
5209
+ setUrlBusy(false);
4836
5210
  };
4837
5211
  const switchToStatic = () => {
4838
5212
  if (isStatic) return;
@@ -4847,10 +5221,12 @@ window.__ModuleLoader__.load({
4847
5221
  };
4848
5222
  const setGenType = (type) => {
4849
5223
  if (type === activeGenType) return;
5224
+ setPaused(false);
4850
5225
  setBgType(type);
4851
5226
  };
4852
5227
  const ensureGenParams = () => generatedBg ?? defaultParamsFor(activeGenType);
4853
5228
  const updateGenerated = (patch) => {
5229
+ setPaused(false);
4854
5230
  setGeneratedBg({
4855
5231
  ...ensureGenParams(),
4856
5232
  ...patch
@@ -4881,9 +5257,13 @@ window.__ModuleLoader__.load({
4881
5257
  case "aurora": return t("presetAurora");
4882
5258
  case "nebula": return t("presetNebula");
4883
5259
  case "noise": return t("presetNoise");
5260
+ case "starfield": return t("presetStarfield");
4884
5261
  case "dots": return t("presetDots");
4885
5262
  case "waves": return t("presetWaves");
4886
5263
  case "poly": return t("presetPoly");
5264
+ case "rain": return t("presetRain");
5265
+ case "contour": return t("presetContour");
5266
+ case "meta": return t("presetMeta");
4887
5267
  default: return key;
4888
5268
  }
4889
5269
  };
@@ -4938,6 +5318,7 @@ window.__ModuleLoader__.load({
4938
5318
  type: "button",
4939
5319
  className: "dab-btn",
4940
5320
  onClick: () => {
5321
+ setPaused(false);
4941
5322
  regenerateBg();
4942
5323
  setSpinTick((x) => x + 1);
4943
5324
  },
@@ -4945,7 +5326,7 @@ window.__ModuleLoader__.load({
4945
5326
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4946
5327
  type: "button",
4947
5328
  className: "dab-btn dab-btn-danger",
4948
- onClick: () => setWp(null),
5329
+ onClick: () => setWpFromServer(null),
4949
5330
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 }), t("bgRemove")]
4950
5331
  })]
4951
5332
  })
@@ -5006,7 +5387,7 @@ window.__ModuleLoader__.load({
5006
5387
  }),
5007
5388
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5008
5389
  type: "button",
5009
- className: "dab-btn dab-btn-ghost",
5390
+ className: "dab-btn dab-btn-soft",
5010
5391
  onClick: () => setUrlOpen((o) => !o),
5011
5392
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkIcon, { size: 14 }), t("bgFromUrl")]
5012
5393
  }),
@@ -5019,21 +5400,14 @@ window.__ModuleLoader__.load({
5019
5400
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 14 }), t("bgEdit")]
5020
5401
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5021
5402
  type: "button",
5022
- className: "dab-btn dab-btn-ghost dab-btn-danger",
5023
- onClick: () => setWp(null),
5403
+ className: "dab-btn dab-btn-danger-inverted",
5404
+ onClick: () => setWpFromServer(null),
5024
5405
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("bgRemove")]
5025
5406
  })] }) : null
5026
5407
  ]
5027
5408
  }),
5028
5409
  urlOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5029
5410
  className: "dab-urlrow",
5030
- style: {
5031
- marginTop: 12,
5032
- display: "flex",
5033
- flexWrap: "wrap",
5034
- gap: 8,
5035
- alignItems: "center"
5036
- },
5037
5411
  children: [
5038
5412
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5039
5413
  type: "text",
@@ -5213,9 +5587,10 @@ window.__ModuleLoader__.load({
5213
5587
  children: [
5214
5588
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5215
5589
  className: "dab-types",
5216
- children: typeMeta.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5590
+ children: typeMeta.map((m, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5217
5591
  type: "button",
5218
5592
  className: `dab-type${activeGenType === m.type ? " is-active" : ""}`,
5593
+ style: { "--i": i },
5219
5594
  onClick: () => setGenType(m.type),
5220
5595
  children: [
5221
5596
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `dab-type-thumb ${m.thumb}` }),
@@ -5244,7 +5619,8 @@ window.__ModuleLoader__.load({
5244
5619
  children: [
5245
5620
  "aurora",
5246
5621
  "nebula",
5247
- "noise"
5622
+ "noise",
5623
+ "starfield"
5248
5624
  ].map((pr) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5249
5625
  type: "button",
5250
5626
  className: `dab-chip${generatedBg.preset === pr ? " is-active" : ""}`,
@@ -5263,7 +5639,10 @@ window.__ModuleLoader__.load({
5263
5639
  children: [
5264
5640
  "dots",
5265
5641
  "waves",
5266
- "poly"
5642
+ "poly",
5643
+ "rain",
5644
+ "contour",
5645
+ "meta"
5267
5646
  ].map((pr) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5268
5647
  type: "button",
5269
5648
  className: `dab-chip${generatedBg.preset === pr ? " is-active" : ""}`,
@@ -5353,11 +5732,26 @@ window.__ModuleLoader__.load({
5353
5732
  onClick: () => setRegenerateOnReload(!regenerateOnReload),
5354
5733
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
5355
5734
  }),
5735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5736
+ type: "button",
5737
+ className: "dab-btn",
5738
+ onClick: () => {
5739
+ if (paused) {
5740
+ resumeGeneratedBg();
5741
+ setPaused(false);
5742
+ } else {
5743
+ pauseGeneratedBg();
5744
+ setPaused(true);
5745
+ }
5746
+ },
5747
+ children: [paused ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayIcon, { size: 13 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PauseIcon, { size: 13 }), paused ? t("bgResume") : t("bgPause")]
5748
+ }),
5356
5749
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5357
5750
  type: "button",
5358
5751
  className: "dab-btn dab-btn-primary",
5359
5752
  onClick: () => {
5360
5753
  regenerateBg();
5754
+ setPaused(false);
5361
5755
  setSpinTick((x) => x + 1);
5362
5756
  },
5363
5757
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -5618,10 +6012,9 @@ window.__ModuleLoader__.load({
5618
6012
  }
5619
6013
  function ProfilePage({ p, notify }) {
5620
6014
  const { t, exportTheme, importTheme, saveProfile, applyProfile, deleteProfile, applyPreset, setSchedule, useStore } = p;
5621
- const store = useStore((s) => s);
5622
- const profiles = store.profiles;
5623
- const schedule = store.schedule;
5624
- const activeProfile = store.activeProfile;
6015
+ const profiles = useStore((s) => s.profiles);
6016
+ const schedule = useStore((s) => s.schedule);
6017
+ const activeProfile = useStore((s) => s.activeProfile);
5625
6018
  const importRef = (0, react.useRef)(null);
5626
6019
  const [nameOpen, setNameOpen] = (0, react.useState)(false);
5627
6020
  const [nameVal, setNameVal] = (0, react.useState)("");
@@ -5694,11 +6087,12 @@ window.__ModuleLoader__.load({
5694
6087
  children: t("presetGalleryTitle")
5695
6088
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5696
6089
  className: "dab-preset-grid",
5697
- children: BUILTIN_PRESETS.map((ps) => {
6090
+ children: BUILTIN_PRESETS.map((ps, i) => {
5698
6091
  const hex = hexOf(ps.appearance.color);
5699
6092
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5700
6093
  type: "button",
5701
6094
  className: "dab-preset",
6095
+ style: { "--i": i },
5702
6096
  onClick: () => {
5703
6097
  applyPreset(ps.appearance);
5704
6098
  notify(t("presetApplied"));
@@ -5741,13 +6135,6 @@ window.__ModuleLoader__.load({
5741
6135
  }),
5742
6136
  nameOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5743
6137
  className: "dab-urlrow",
5744
- style: {
5745
- marginTop: 12,
5746
- display: "flex",
5747
- flexWrap: "wrap",
5748
- gap: 8,
5749
- alignItems: "center"
5750
- },
5751
6138
  children: [
5752
6139
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5753
6140
  type: "text",
@@ -5788,8 +6175,9 @@ window.__ModuleLoader__.load({
5788
6175
  children: t("profilesEmpty")
5789
6176
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5790
6177
  className: "dab-profile-list",
5791
- children: profiles.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6178
+ children: profiles.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5792
6179
  className: `dab-profile-row${activeProfile === entry.id ? " is-active" : ""}`,
6180
+ style: { "--i": i },
5793
6181
  children: [
5794
6182
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfileDot, { entry }),
5795
6183
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5810,10 +6198,10 @@ window.__ModuleLoader__.load({
5810
6198
  }),
5811
6199
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5812
6200
  type: "button",
5813
- className: `dab-btn dab-btn-ghost dab-btn-danger${confirmId === entry.id ? " dab-btn-confirm" : ""}`,
6201
+ className: `dab-btn dab-btn-danger-solid${confirmId === entry.id ? " dab-btn-confirm" : ""}`,
5814
6202
  title: t("profileDelete"),
5815
6203
  onClick: () => onDelete(entry),
5816
- children: confirmId === entry.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 13 }), t("profileDeleteConfirm")] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 })
6204
+ children: confirmId === entry.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 13 }), t("profileDeleteConfirm")] }) : t("profileDelete")
5817
6205
  })
5818
6206
  ]
5819
6207
  }, entry.id))
@@ -5844,93 +6232,103 @@ window.__ModuleLoader__.load({
5844
6232
  style: { marginTop: 8 },
5845
6233
  children: t("scheduleHint")
5846
6234
  }),
5847
- schedule.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5848
- className: "dab-schedule-grid",
5849
- children: [
5850
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5851
- className: "dab-schedule-cell",
5852
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5853
- className: "dab-swatch-title",
5854
- children: t("scheduleMode")
5855
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5856
- className: "dab-chip-row",
5857
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5858
- type: "button",
5859
- className: `dab-chip${schedule.mode === "time" ? " is-active" : ""}`,
5860
- onClick: () => patchSchedule({ mode: "time" }),
5861
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClockIcon, { size: 13 }), t("scheduleModeTime")]
5862
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5863
- type: "button",
5864
- className: `dab-chip${schedule.mode === "system" ? " is-active" : ""}`,
5865
- onClick: () => patchSchedule({ mode: "system" }),
5866
- children: t("scheduleModeSystem")
5867
- })]
5868
- })]
5869
- }),
5870
- schedule.mode === "time" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5871
- className: "dab-schedule-cell",
5872
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5873
- className: "dab-swatch-title",
5874
- children: t("scheduleTimes")
5875
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5876
- className: "dab-time-row",
5877
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5878
- className: "dab-time-label",
5879
- children: [
5880
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }),
5881
- t("scheduleDayStart"),
5882
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5883
- type: "time",
5884
- className: "dab-timeinput",
5885
- value: schedule.dayStart,
5886
- onChange: (e) => {
5887
- if (/^([01]\d|2[0-3]):[0-5]\d$/.test(e.target.value)) patchSchedule({ dayStart: e.target.value });
5888
- }
5889
- })
5890
- ]
5891
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5892
- className: "dab-time-label",
5893
- children: [
5894
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MoonIcon, { size: 13 }),
5895
- t("scheduleNightStart"),
5896
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5897
- type: "time",
5898
- className: "dab-timeinput",
5899
- value: schedule.nightStart,
5900
- onChange: (e) => {
5901
- if (/^([01]\d|2[0-3]):[0-5]\d$/.test(e.target.value)) patchSchedule({ nightStart: e.target.value });
5902
- }
5903
- })
5904
- ]
5905
- })]
5906
- })]
5907
- }) : null,
5908
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5909
- className: "dab-schedule-cell",
5910
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5911
- className: "dab-swatch-title",
5912
- children: t("scheduleProfiles")
5913
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5914
- className: "dab-time-row",
5915
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5916
- className: "dab-time-label",
5917
- children: [
5918
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }),
5919
- t("scheduleDayProfile"),
5920
- profileOptions(schedule.dayProfile, (id) => patchSchedule({ dayProfile: id }))
5921
- ]
5922
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5923
- className: "dab-time-label",
5924
- children: [
5925
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MoonIcon, { size: 13 }),
5926
- t("scheduleNightProfile"),
5927
- profileOptions(schedule.nightProfile, (id) => patchSchedule({ nightProfile: id }))
5928
- ]
5929
- })]
5930
- })]
6235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6236
+ className: `dab-schedule-wrap${schedule.enabled ? " is-open" : ""}`,
6237
+ "aria-hidden": !schedule.enabled,
6238
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6239
+ className: "dab-schedule-clip",
6240
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6241
+ className: "dab-schedule-grid",
6242
+ children: [
6243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6244
+ className: "dab-schedule-cell",
6245
+ style: { "--i": 0 },
6246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6247
+ className: "dab-swatch-title",
6248
+ children: t("scheduleMode")
6249
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6250
+ className: "dab-chip-row",
6251
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6252
+ type: "button",
6253
+ className: `dab-chip${schedule.mode === "time" ? " is-active" : ""}`,
6254
+ onClick: () => patchSchedule({ mode: "time" }),
6255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClockIcon, { size: 13 }), t("scheduleModeTime")]
6256
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6257
+ type: "button",
6258
+ className: `dab-chip${schedule.mode === "system" ? " is-active" : ""}`,
6259
+ onClick: () => patchSchedule({ mode: "system" }),
6260
+ children: t("scheduleModeSystem")
6261
+ })]
6262
+ })]
6263
+ }),
6264
+ schedule.mode === "time" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6265
+ className: "dab-schedule-cell",
6266
+ style: { "--i": 1 },
6267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6268
+ className: "dab-swatch-title",
6269
+ children: t("scheduleTimes")
6270
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6271
+ className: "dab-time-row",
6272
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6273
+ className: "dab-time-label",
6274
+ children: [
6275
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }),
6276
+ t("scheduleDayStart"),
6277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6278
+ type: "time",
6279
+ className: "dab-timeinput",
6280
+ value: schedule.dayStart,
6281
+ onChange: (e) => {
6282
+ if (/^([01]\d|2[0-3]):[0-5]\d$/.test(e.target.value)) patchSchedule({ dayStart: e.target.value });
6283
+ }
6284
+ })
6285
+ ]
6286
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6287
+ className: "dab-time-label",
6288
+ children: [
6289
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MoonIcon, { size: 13 }),
6290
+ t("scheduleNightStart"),
6291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6292
+ type: "time",
6293
+ className: "dab-timeinput",
6294
+ value: schedule.nightStart,
6295
+ onChange: (e) => {
6296
+ if (/^([01]\d|2[0-3]):[0-5]\d$/.test(e.target.value)) patchSchedule({ nightStart: e.target.value });
6297
+ }
6298
+ })
6299
+ ]
6300
+ })]
6301
+ })]
6302
+ }) : null,
6303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6304
+ className: "dab-schedule-cell",
6305
+ style: { "--i": 2 },
6306
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6307
+ className: "dab-swatch-title",
6308
+ children: t("scheduleProfiles")
6309
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6310
+ className: "dab-time-row",
6311
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6312
+ className: "dab-time-label",
6313
+ children: [
6314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }),
6315
+ t("scheduleDayProfile"),
6316
+ profileOptions(schedule.dayProfile, (id) => patchSchedule({ dayProfile: id }))
6317
+ ]
6318
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6319
+ className: "dab-time-label",
6320
+ children: [
6321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MoonIcon, { size: 13 }),
6322
+ t("scheduleNightProfile"),
6323
+ profileOptions(schedule.nightProfile, (id) => patchSchedule({ nightProfile: id }))
6324
+ ]
6325
+ })]
6326
+ })]
6327
+ })
6328
+ ]
5931
6329
  })
5932
- ]
5933
- }) : null
6330
+ })
6331
+ })
5934
6332
  ]
5935
6333
  }),
5936
6334
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -6186,7 +6584,7 @@ window.__ModuleLoader__.load({
6186
6584
  }, "dsh-any-background: skin dispose");
6187
6585
  const styleEl = document.createElement("style");
6188
6586
  styleEl.dataset.plugin = "dsh-any-background";
6189
- styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
6587
+ styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}${MOBILE_HEADER_RULE}[data-composer-card] textarea::placeholder,[data-composer-card] input::placeholder,[data-composer-card] [contenteditable]::placeholder,[data-cordis-panel] input::placeholder,[data-cordis-panel] textarea::placeholder,.dab-input::placeholder,.dab-input textarea::placeholder,.dab-input input::placeholder{color:var(--dsh-any-placeholder,var(--dsw-alias-label-caption,#8a8f98))!important;font-style:italic;opacity:.85}`;
6190
6588
  document.head.appendChild(styleEl);
6191
6589
  ctx.effect(() => () => {
6192
6590
  styleEl?.parentNode?.removeChild(styleEl);
@@ -6291,32 +6689,35 @@ window.__ModuleLoader__.load({
6291
6689
  * the full adaptation (host skin + editor wheel sync); the caller persists. */
6292
6690
  const adoptWallpaperColor = async (dataUrl) => {
6293
6691
  const hsl = await extractWallpaperColor(dataUrl, rBgState());
6294
- if (!hsl) return;
6692
+ if (!hsl || rWp() !== dataUrl) return;
6295
6693
  cfg.color = hsl;
6296
6694
  registerCustom(hsl[0], hsl[1], hsl[2]);
6297
6695
  colorRev++;
6298
6696
  bound?.syncColor(hslToHsv(hsl[0], hsl[1], hsl[2]), colorRev);
6299
6697
  };
6300
6698
  /** Activate a rotation item: the server copies its bytes into the wallpaper
6301
- * slot; the client applies the returned data URL through the normal image
6699
+ * slot; the client applies the returned serve URL through the normal image
6302
6700
  * path, then re-extracts the theme color from the new picture so the
6303
6701
  * palette follows the rotation. */
6304
6702
  const applyRotationIndex = async (idx, auto) => {
6305
6703
  const rot = rRotation();
6306
6704
  if (idx < 0 || idx >= rot.items.length) return false;
6307
6705
  const r = await rotationActivate(idx);
6308
- if (!r.ok || !r.dataUrl) {
6706
+ if (!r.ok || !r.wallpaperUrl) {
6309
6707
  console.warn("dsh-any-background: rotation activate failed", r.error);
6310
6708
  return false;
6311
6709
  }
6312
- setWpImageUrl(r.dataUrl);
6313
- if (cfg.backgroundType === "image") setWpUrl(r.dataUrl);
6710
+ cfg.backgroundType = "image";
6711
+ setBgDark(null);
6712
+ setWpImageUrl(r.wallpaperUrl);
6713
+ setWpUrl(rWpImage());
6714
+ setBgState({ ...DEFAULT_CONFIG.bgState });
6314
6715
  cfg.rotation = {
6315
6716
  ...rot,
6316
6717
  current: idx,
6317
6718
  lastRotate: auto || rot.lastRotate === null ? (/* @__PURE__ */ new Date()).toISOString() : rot.lastRotate
6318
6719
  };
6319
- if (cfg.backgroundType === "image") await adoptWallpaperColor(r.dataUrl);
6720
+ if (cfg.backgroundType === "image") await adoptWallpaperColor(rWp());
6320
6721
  applyThemeColor();
6321
6722
  syncBg();
6322
6723
  saveConfig();
@@ -6510,16 +6911,16 @@ window.__ModuleLoader__.load({
6510
6911
  nl
6511
6912
  ], colorRev);
6512
6913
  },
6513
- setWp: (u) => {
6914
+ setWpFromServer: (u) => {
6514
6915
  cfg.backgroundType = "image";
6515
6916
  setBgDark(null);
6516
6917
  setWpImageUrl(u);
6517
- setWpUrl(u);
6918
+ setWpUrl(rWpImage());
6518
6919
  setBgState({ ...DEFAULT_CONFIG.bgState });
6519
- persistWallpaper(u);
6520
6920
  if (u === null) {
6521
6921
  setWpVideoUrl(null, null);
6522
6922
  persistVideo(null);
6923
+ persistWallpaper(null);
6523
6924
  }
6524
6925
  applyThemeColor();
6525
6926
  syncBg();
@@ -6624,23 +7025,29 @@ window.__ModuleLoader__.load({
6624
7025
  if (cfg.backgroundType === "video") {
6625
7026
  const vurl = rWpVideo();
6626
7027
  if (vurl) try {
6627
- const blob = await fetch(vurl).then((r) => r.blob());
6628
- videoPayload = await new Promise((resolve, reject) => {
6629
- const fr = new FileReader();
6630
- fr.onload = () => resolve(fr.result);
6631
- fr.onerror = () => reject(fr.error);
6632
- fr.readAsDataURL(blob);
6633
- });
7028
+ videoPayload = await blobToDataUrl(await fetch(vurl).then((r) => r.blob()));
6634
7029
  if (videoPayload && !/^data:video\//.test(videoPayload)) videoPayload = videoPayload.replace(/^data:[^;,]*/, `data:${cfg.videoMime ?? "video/mp4"}`);
6635
7030
  } catch {
6636
7031
  videoPayload = null;
6637
7032
  }
6638
7033
  }
7034
+ let wallpaperPayload = null;
7035
+ if (cfg.backgroundType === "image") {
7036
+ const wurl = rWp();
7037
+ if (wurl) {
7038
+ if (wurl.startsWith("data:")) wallpaperPayload = wurl;
7039
+ else try {
7040
+ wallpaperPayload = await blobToDataUrl(await fetch(wurl).then((r) => r.blob()));
7041
+ } catch {
7042
+ wallpaperPayload = null;
7043
+ }
7044
+ }
7045
+ }
6639
7046
  const payload = {
6640
7047
  version: 2,
6641
7048
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
6642
7049
  config: cfg,
6643
- wallpaper: cfg.backgroundType === "image" ? rWp() : null,
7050
+ wallpaper: wallpaperPayload,
6644
7051
  video: videoPayload
6645
7052
  };
6646
7053
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
@@ -6821,6 +7228,29 @@ window.__ModuleLoader__.load({
6821
7228
  rotateNow: async () => {
6822
7229
  if (rRotation().items.length === 0) return false;
6823
7230
  return applyRotationIndex(pickNextRotationIndex(), true);
7231
+ },
7232
+ setVideoFromUrl: async (url) => {
7233
+ const r = await setVideoFromUrl(url);
7234
+ if (!r.ok || !r.mime) {
7235
+ console.warn("dsh-any-background: video url fetch failed", r.error);
7236
+ return false;
7237
+ }
7238
+ setBgDark(null);
7239
+ cfg.backgroundType = "video";
7240
+ setWpUrl(null);
7241
+ cfg.videoBgState = { ...DEFAULT_CONFIG.bgState };
7242
+ setWpVideoUrl(VIDEO_SERVE_URL, r.mime);
7243
+ applyWp();
7244
+ saveConfig();
7245
+ syncBg();
7246
+ const applied = rWpVideo();
7247
+ captureVideoSnapshot(applied).then((snap) => {
7248
+ if (rWpVideo() !== applied) return;
7249
+ setWpVideoSnapshot(snap);
7250
+ applyThemeColor();
7251
+ syncBg();
7252
+ });
7253
+ return true;
6824
7254
  }
6825
7255
  };
6826
7256
  };