dsh-any-background 0.2.4 → 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
@@ -29,7 +29,7 @@ window.__ModuleLoader__.load({
29
29
  descColor: "拖动色轮或输入精确数值,整个界面的配色将实时跟随变化",
30
30
  descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感",
31
31
  descBackground: "上传一张图片或一段视频作为壁纸并自由摆放,或让算法为你实时生成动态背景",
32
- descProfile: "将当前主题导出为文件备份,或从文件一键恢复完整配置",
32
+ descProfile: "一键套用外观预设,保存多套配置随时切换,设置昼夜自动换装,或导出/导入完整主题",
33
33
  colorTitle: "主题色",
34
34
  colorHint: "在色轮外圈选择色相,在内部方形中调整饱和度和明度;双击滑块可恢复默认值",
35
35
  swatchTitle: "灵感色板",
@@ -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: "加载中…",
@@ -121,7 +121,65 @@ window.__ModuleLoader__.load({
121
121
  presetNoise: "流动噪声",
122
122
  presetDots: "点阵",
123
123
  presetWaves: "波浪",
124
- presetPoly: "低多边形"
124
+ presetPoly: "低多边形",
125
+ presetStarfield: "星空",
126
+ presetRain: "雨滴",
127
+ presetContour: "等高线",
128
+ presetMeta: "流体光斑",
129
+ bgPause: "暂停",
130
+ bgResume: "播放",
131
+ bgUrlVideoFail: "获取视频失败,请确认链接可直接下载视频文件",
132
+ schemeTitle: "界面明暗",
133
+ schemeLight: "亮色",
134
+ schemeDark: "暗色",
135
+ schemeAuto: "自动",
136
+ schemeHint: "「自动」优先跟随背景画面的实际明暗决定界面明暗(无背景时依据主题色明度);强制亮色/暗色后整套配色按所选方向生成,未选主题色时使用中性灰配色",
137
+ presetGalleryTitle: "外观预设",
138
+ presetName_default: "默认",
139
+ presetDesc_default: "恢复插件的初始外观",
140
+ presetName_glass: "毛玻璃",
141
+ presetDesc_glass: "通透表面 + 磨砂玻璃层次",
142
+ presetName_minimal: "极简白",
143
+ presetDesc_minimal: "接近实色的表面,无模糊",
144
+ presetName_midnight: "暗夜紫",
145
+ presetDesc_midnight: "深色强调,重磨砂低透明度",
146
+ presetName_cyber: "赛博",
147
+ presetDesc_cyber: "艳丽洋红,界面近乎消失",
148
+ presetName_warm: "暖阳",
149
+ presetDesc_warm: "琥珀色调的明亮实色界面",
150
+ presetApplied: "预设已应用",
151
+ profilesTitle: "我的配置",
152
+ profileSave: "保存当前配置",
153
+ profileNamePlaceholder: "输入配置名称…",
154
+ profileNameRequired: "请先输入配置名称",
155
+ profileSaved: "配置已保存",
156
+ profileApplied: "配置已应用",
157
+ profileApply: "应用",
158
+ profileDelete: "删除配置",
159
+ profileDeleteConfirm: "确认删除",
160
+ profileNone: "不切换",
161
+ profilesEmpty: "还没有保存的配置。调整好外观后点击「保存当前配置」,即可随时一键切换。",
162
+ scheduleTitle: "昼夜自动切换",
163
+ scheduleHint: "按时间或系统深色模式,在两套已保存的配置之间自动切换(切换的是配色与表面参数,壁纸不变)",
164
+ scheduleMode: "切换依据",
165
+ scheduleModeTime: "固定时段",
166
+ scheduleModeSystem: "跟随系统",
167
+ scheduleTimes: "切换时间",
168
+ scheduleDayStart: "日间开始",
169
+ scheduleNightStart: "夜间开始",
170
+ scheduleProfiles: "对应配置",
171
+ scheduleDayProfile: "日间",
172
+ scheduleNightProfile: "夜间",
173
+ rotTitle: "壁纸轮换",
174
+ rotHint: "把多张图片加入轮换池,按所选频率自动更换壁纸;启用后立即生效",
175
+ rotAdd: "添加图片",
176
+ rotRemove: "移除",
177
+ rotShuffle: "随机",
178
+ rotOrder: "顺序",
179
+ rotReload: "每次刷新",
180
+ rotDaily: "每天",
181
+ rotWeekly: "每周",
182
+ rotNow: "立即切换"
125
183
  };
126
184
  const en = {
127
185
  nav: "Theme",
@@ -134,7 +192,7 @@ window.__ModuleLoader__.load({
134
192
  descColor: "Drag the wheel or type exact values — the whole UI follows live",
135
193
  descInterface: "Tune opacity and blur per surface to build depth",
136
194
  descBackground: "Upload an image or a video as wallpaper and place it freely, or let the algorithm paint a live background",
137
- descProfile: "Export the current theme as a file, or restore it from one",
195
+ descProfile: "Apply a one-click preset, save multiple named profiles, schedule a day/night switch, or export/import the full theme",
138
196
  colorTitle: "Theme color",
139
197
  colorHint: "Pick hue on the outer ring, adjust saturation & lightness in the square; double-click a slider to reset",
140
198
  swatchTitle: "Quick swatches",
@@ -153,7 +211,7 @@ window.__ModuleLoader__.load({
153
211
  bgChoose: "Choose image or video",
154
212
  bgRemove: "Remove background",
155
213
  bgFromUrl: "From URL",
156
- bgUrlPlaceholder: "Paste an image URL https://…",
214
+ bgUrlPlaceholder: "Paste an image or video URL https://…",
157
215
  bgUrlApply: "Apply",
158
216
  bgUrlCancel: "Cancel",
159
217
  bgUrlApplying: "Loading…",
@@ -226,7 +284,65 @@ window.__ModuleLoader__.load({
226
284
  presetNoise: "Flowing noise",
227
285
  presetDots: "Dots",
228
286
  presetWaves: "Waves",
229
- presetPoly: "Low poly"
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",
295
+ schemeTitle: "Interface scheme",
296
+ schemeLight: "Light",
297
+ schemeDark: "Dark",
298
+ schemeAuto: "Auto",
299
+ schemeHint: "\"Auto\" follows the background brightness first (accent lightness as fallback only); forcing light/dark regenerates the palette in that direction - with no accent picked, a neutral gray palette is used",
300
+ presetGalleryTitle: "Appearance presets",
301
+ presetName_default: "Default",
302
+ presetDesc_default: "Restore the plugin's original look",
303
+ presetName_glass: "Frosted glass",
304
+ presetDesc_glass: "Translucent surfaces + frosted depth",
305
+ presetName_minimal: "Minimal",
306
+ presetDesc_minimal: "Near-solid surfaces, no frost",
307
+ presetName_midnight: "Midnight",
308
+ presetDesc_midnight: "Deep violet accent, heavy frost",
309
+ presetName_cyber: "Cyber",
310
+ presetDesc_cyber: "Vivid magenta, vanishing surfaces",
311
+ presetName_warm: "Warm daylight",
312
+ presetDesc_warm: "Amber accent on solid light surfaces",
313
+ presetApplied: "Preset applied",
314
+ profilesTitle: "Saved profiles",
315
+ profileSave: "Save current",
316
+ profileNamePlaceholder: "Profile name…",
317
+ profileNameRequired: "Enter a name first",
318
+ profileSaved: "Profile saved",
319
+ profileApplied: "Profile applied",
320
+ profileApply: "Apply",
321
+ profileDelete: "Delete profile",
322
+ profileDeleteConfirm: "Sure?",
323
+ profileNone: "None",
324
+ profilesEmpty: "No saved profiles yet. Tune the look, then hit \"Save current\" to switch back anytime.",
325
+ scheduleTitle: "Day/night auto switch",
326
+ scheduleHint: "Automatically swap between two saved profiles by clock time or the OS dark mode (colors and surfaces switch; the wallpaper stays)",
327
+ scheduleMode: "Trigger",
328
+ scheduleModeTime: "Fixed times",
329
+ scheduleModeSystem: "Follow system",
330
+ scheduleTimes: "Switch times",
331
+ scheduleDayStart: "Day starts",
332
+ scheduleNightStart: "Night starts",
333
+ scheduleProfiles: "Profiles",
334
+ scheduleDayProfile: "Day",
335
+ scheduleNightProfile: "Night",
336
+ rotTitle: "Wallpaper rotation",
337
+ rotHint: "Add images to a rotation pool and let the wallpaper change on your cadence; takes effect immediately when enabled",
338
+ rotAdd: "Add images",
339
+ rotRemove: "Remove",
340
+ rotShuffle: "Shuffle",
341
+ rotOrder: "In order",
342
+ rotReload: "Every refresh",
343
+ rotDaily: "Daily",
344
+ rotWeekly: "Weekly",
345
+ rotNow: "Switch now"
230
346
  };
231
347
  //#endregion
232
348
  //#region src/client/state.ts
@@ -270,7 +386,26 @@ window.__ModuleLoader__.load({
270
386
  generatedBg: null,
271
387
  regenerateOnReload: false,
272
388
  chatTextOpacity: 0,
273
- trajectoryOpacity: 1
389
+ trajectoryOpacity: 1,
390
+ profiles: [],
391
+ rotation: {
392
+ enabled: false,
393
+ mode: "shuffle",
394
+ interval: "daily",
395
+ current: 0,
396
+ items: [],
397
+ lastRotate: null
398
+ },
399
+ schedule: {
400
+ enabled: false,
401
+ mode: "time",
402
+ dayProfile: null,
403
+ nightProfile: null,
404
+ dayStart: "07:00",
405
+ nightStart: "19:00"
406
+ },
407
+ schemeOverride: "auto",
408
+ activeProfile: null
274
409
  };
275
410
  const clamp01 = (n, def) => typeof n === "number" ? Math.min(1, Math.max(0, n)) : def;
276
411
  let cfg = {
@@ -287,12 +422,19 @@ window.__ModuleLoader__.load({
287
422
  let wpVideoSnapshot = null;
288
423
  /** Local blob URL backing an in-session video; revoked when replaced or cleared. */
289
424
  let wpVideoObjectUrl = null;
290
- function setWpImageUrl(url) {
291
- wpImageUrl = url;
292
- }
293
425
  function setWpUrl(url) {
294
426
  wpUrl = url;
295
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
+ }
296
438
  let videoRev = 0;
297
439
  function setWpVideoUrl(url, mime) {
298
440
  if (url === null) {
@@ -403,8 +545,144 @@ window.__ModuleLoader__.load({
403
545
  function rVideoBgState() {
404
546
  return cfg.videoBgState;
405
547
  }
548
+ function rProfiles() {
549
+ return Array.isArray(cfg.profiles) ? cfg.profiles : [];
550
+ }
551
+ function rRotation() {
552
+ const r = cfg.rotation;
553
+ return r && typeof r === "object" ? {
554
+ ...DEFAULT_CONFIG.rotation,
555
+ ...r,
556
+ items: Array.isArray(r.items) ? r.items : []
557
+ } : {
558
+ ...DEFAULT_CONFIG.rotation,
559
+ items: []
560
+ };
561
+ }
562
+ function rSchedule() {
563
+ return cfg.schedule && typeof cfg.schedule === "object" ? {
564
+ ...DEFAULT_CONFIG.schedule,
565
+ ...cfg.schedule
566
+ } : { ...DEFAULT_CONFIG.schedule };
567
+ }
568
+ function rSchemeOverride() {
569
+ return cfg.schemeOverride === "light" || cfg.schemeOverride === "dark" ? cfg.schemeOverride : "auto";
570
+ }
571
+ /** Effective interface scheme (drives data-ds-dark-theme / color-scheme): a
572
+ * forced override wins; in auto a picked color decides through its palette
573
+ * direction (keeps native controls aligned with the drawn surfaces), and
574
+ * without a pick the background's brightness verdict does. Stay light as the
575
+ * last fallback. */
576
+ function rScheme() {
577
+ const o = rSchemeOverride();
578
+ if (o !== "auto") return o;
579
+ if (rHasColor()) return rColorScheme();
580
+ return rBgDark() ? "dark" : "light";
581
+ }
582
+ /** Direction the color palette itself is built in: a forced override wins; in
583
+ * auto the theme color's own lightness decides (0.2.4 behavior) so surfaces
584
+ * always contrast with the palette's label fonts — the wallpaper verdict must
585
+ * not drag a dark color's surfaces into a light build (and vice versa) or
586
+ * text and surfaces converge. */
587
+ function rColorScheme() {
588
+ const o = rSchemeOverride();
589
+ if (o !== "auto") return o;
590
+ return rHasColor() && rColor()[2] < .55 ? "dark" : "light";
591
+ }
592
+ /** Snapshot of the appearance fields a profile/preset restores. */
593
+ function currentAppearance() {
594
+ return {
595
+ color: cfg.color,
596
+ opacities: { ...rOps() },
597
+ blurs: { ...rBlurs() },
598
+ settingsOpacity: rSop(),
599
+ wallpaperOpacity: rWop(),
600
+ blur: rBl(),
601
+ chatTextOpacity: rChatTextOpacity(),
602
+ trajectoryOpacity: rTrajectoryOpacity()
603
+ };
604
+ }
605
+ /** Apply an appearance snapshot onto cfg (meta fields untouched). */
606
+ function applyAppearance(ap) {
607
+ cfg.color = Array.isArray(ap.color) && ap.color.length === 3 ? [...ap.color] : null;
608
+ cfg.opacities = {
609
+ ...DEFAULT_CONFIG.opacities,
610
+ ...ap.opacities ?? {}
611
+ };
612
+ cfg.blurs = {
613
+ ...DEFAULT_CONFIG.blurs,
614
+ ...ap.blurs ?? {}
615
+ };
616
+ cfg.settingsOpacity = clamp01(ap.settingsOpacity, DEFAULT_CONFIG.settingsOpacity);
617
+ cfg.wallpaperOpacity = clamp01(ap.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity);
618
+ cfg.blur = typeof ap.blur === "number" ? Math.min(60, Math.max(0, ap.blur)) : DEFAULT_CONFIG.blur;
619
+ cfg.chatTextOpacity = clamp01(ap.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity);
620
+ cfg.trajectoryOpacity = clamp01(ap.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity);
621
+ }
406
622
  const num = (n, def) => typeof n === "number" ? n : def;
407
623
  const cl = (n, lo, hi, def) => typeof n === "number" ? Math.min(hi, Math.max(lo, n)) : def;
624
+ const HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
625
+ function adoptProfiles(raw) {
626
+ if (!Array.isArray(raw)) return [];
627
+ const out = [];
628
+ for (const item of raw.slice(0, 20)) {
629
+ const p = item ?? {};
630
+ if (typeof p.id !== "string" || p.id.length === 0 || p.id.length > 64) continue;
631
+ if (out.some((e) => e.id === p.id)) continue;
632
+ const ac = p.config ?? {};
633
+ out.push({
634
+ id: p.id,
635
+ name: typeof p.name === "string" && p.name.trim() ? p.name.slice(0, 60) : "Profile",
636
+ createdAt: typeof p.createdAt === "string" ? p.createdAt : "",
637
+ config: {
638
+ color: Array.isArray(ac.color) && ac.color.length === 3 ? [...ac.color] : null,
639
+ opacities: {
640
+ ...DEFAULT_CONFIG.opacities,
641
+ ...ac.opacities ?? {}
642
+ },
643
+ blurs: {
644
+ ...DEFAULT_CONFIG.blurs,
645
+ ...ac.blurs ?? {}
646
+ },
647
+ settingsOpacity: clamp01(ac.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
648
+ wallpaperOpacity: clamp01(ac.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity),
649
+ blur: num(ac.blur, DEFAULT_CONFIG.blur),
650
+ chatTextOpacity: clamp01(ac.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
651
+ trajectoryOpacity: clamp01(ac.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity)
652
+ }
653
+ });
654
+ }
655
+ return out;
656
+ }
657
+ function adoptRotation(raw) {
658
+ const r = raw ?? {};
659
+ const items = Array.isArray(r.items) ? r.items.filter((it) => {
660
+ const i = it ?? {};
661
+ return typeof i?.file === "string" && /^[\w-]+\.(jpg|jpeg|png|gif|webp)$/i.test(i.file);
662
+ }).slice(0, 30).map((it) => ({
663
+ file: it.file,
664
+ thumb: typeof it.thumb === "string" && it.thumb.startsWith("data:image/") && it.thumb.length <= 65536 ? it.thumb : ""
665
+ })) : [];
666
+ return {
667
+ enabled: r.enabled === true,
668
+ mode: r.mode === "order" ? "order" : "shuffle",
669
+ interval: r.interval === "reload" || r.interval === "weekly" ? r.interval : "daily",
670
+ current: num(r.current, 0),
671
+ items,
672
+ lastRotate: typeof r.lastRotate === "string" ? r.lastRotate : null
673
+ };
674
+ }
675
+ function adoptSchedule(raw) {
676
+ const r = raw ?? {};
677
+ return {
678
+ enabled: r.enabled === true,
679
+ mode: r.mode === "system" ? "system" : "time",
680
+ dayProfile: typeof r.dayProfile === "string" ? r.dayProfile : null,
681
+ nightProfile: typeof r.nightProfile === "string" ? r.nightProfile : null,
682
+ dayStart: typeof r.dayStart === "string" && HHMM_RE.test(r.dayStart) ? r.dayStart : DEFAULT_CONFIG.schedule.dayStart,
683
+ nightStart: typeof r.nightStart === "string" && HHMM_RE.test(r.nightStart) ? r.nightStart : DEFAULT_CONFIG.schedule.nightStart
684
+ };
685
+ }
408
686
  function adoptBgState(s) {
409
687
  return {
410
688
  zoom: num(s.zoom, 1),
@@ -470,7 +748,12 @@ window.__ModuleLoader__.load({
470
748
  generatedBg: generatedBg ? normalizeGeneratedBg(generatedBg) : null,
471
749
  regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
472
750
  chatTextOpacity: clamp01(c.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
473
- trajectoryOpacity: clamp01(c.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity)
751
+ trajectoryOpacity: clamp01(c.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity),
752
+ profiles: adoptProfiles(c.profiles),
753
+ rotation: adoptRotation(c.rotation),
754
+ schedule: adoptSchedule(c.schedule),
755
+ schemeOverride: c.schemeOverride === "light" || c.schemeOverride === "dark" ? c.schemeOverride : "auto",
756
+ activeProfile: typeof c.activeProfile === "string" ? c.activeProfile : null
474
757
  };
475
758
  }
476
759
  function normalizeGeneratedBg(p) {
@@ -486,7 +769,8 @@ window.__ModuleLoader__.load({
486
769
  preset: [
487
770
  "aurora",
488
771
  "nebula",
489
- "noise"
772
+ "noise",
773
+ "starfield"
490
774
  ].includes(p.preset) ? p.preset : "aurora",
491
775
  speed: cl(p.speed, 0, 2, .3),
492
776
  scale: cl(p.scale, .3, 3, 1),
@@ -497,7 +781,10 @@ window.__ModuleLoader__.load({
497
781
  preset: [
498
782
  "dots",
499
783
  "waves",
500
- "poly"
784
+ "poly",
785
+ "rain",
786
+ "contour",
787
+ "meta"
501
788
  ].includes(p.preset) ? p.preset : "dots",
502
789
  density: cl(p.density, 0, 1, .5),
503
790
  scale: cl(p.scale, .3, 3, 1),
@@ -509,6 +796,10 @@ window.__ModuleLoader__.load({
509
796
  const RPC_CHANNEL = "/dsh-any-background";
510
797
  /** Same-origin serve URL of the persisted video (enough for <video src>/fetch). */
511
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";
512
803
  /** HTTP route new videos are POSTed to as raw bytes (see uploadVideo). */
513
804
  const VIDEO_UPLOAD_URL = "/dsh-any-background/video/upload";
514
805
  const RPC_NS = "dshAnyBackground";
@@ -548,18 +839,23 @@ window.__ModuleLoader__.load({
548
839
  function persistConfig() {
549
840
  rpcCall("writeConfig", { config: cfg });
550
841
  }
551
- /** Load the persisted theme (config + wallpaper + video URL) from the node half. */
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. */
552
846
  async function loadPersisted() {
553
847
  const data = await rpcCall("read", {});
554
848
  if (data && typeof data === "object") {
555
849
  const d = data;
556
850
  if (d.config) adoptConfig(d.config);
557
- if (typeof d.wallpaper === "string") setWpImageUrl(d.wallpaper);
558
- else if (d.wallpaper === null) setWpImageUrl(null);
851
+ if (typeof d.wallpaperUrl === "string") setWpImageUrl(d.wallpaperUrl);
852
+ else if (d.wallpaperUrl === null) setWpImageUrl(null);
559
853
  if (typeof d.videoUrl === "string") setWpVideoUrl(d.videoUrl, cfg.videoMime);
560
854
  else if (d.videoUrl === null) setWpVideoUrl(null, null);
561
- if (cfg.backgroundType === "image") setWpUrl(d.wallpaper === null ? null : d.wallpaper);
855
+ if (cfg.backgroundType === "image") setWpUrl(rWpImage());
856
+ return d.rotated === true;
562
857
  }
858
+ return false;
563
859
  }
564
860
  /** Persist a wallpaper (null removes it); one-shot, no debounce. */
565
861
  function persistWallpaper(dataUrl) {
@@ -571,7 +867,7 @@ window.__ModuleLoader__.load({
571
867
  return await rpcCall("setVideo", { dataUrl }) === true;
572
868
  }
573
869
  /** Download a wallpaper from a network URL and persist it into the local slot
574
- * (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
575
871
  * success, or the host's failure message. */
576
872
  async function setWallpaperFromUrl(url) {
577
873
  if (!rpcCallFn) return {
@@ -598,7 +894,40 @@ window.__ModuleLoader__.load({
598
894
  const v = res.value;
599
895
  return v?.ok === true ? {
600
896
  ok: true,
601
- 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"
602
931
  } : {
603
932
  ok: false,
604
933
  error: v?.error ?? "failed"
@@ -618,6 +947,175 @@ window.__ModuleLoader__.load({
618
947
  return false;
619
948
  }
620
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
+ }
964
+ /** Add an image (data URL + small thumbnail) to the server-side rotation pool. */
965
+ async function rotationAdd(dataUrl, thumb) {
966
+ if (!rpcCallFn) return {
967
+ ok: false,
968
+ error: "rpc not ready"
969
+ };
970
+ try {
971
+ const res = await rpcCallFn(rpcEndpoint("rotationAdd"), {
972
+ dataUrl,
973
+ thumb
974
+ });
975
+ if (res && res.ok === true) return res.value ?? {
976
+ ok: false,
977
+ error: "no value"
978
+ };
979
+ return {
980
+ ok: false,
981
+ error: res?.error?.message ?? "request failed"
982
+ };
983
+ } catch (e) {
984
+ return {
985
+ ok: false,
986
+ error: e instanceof Error ? e.message : String(e)
987
+ };
988
+ }
989
+ }
990
+ /** Remove a rotation item by index. Returns the updated items list. */
991
+ async function rotationRemove(index) {
992
+ if (!rpcCallFn) return {
993
+ ok: false,
994
+ error: "rpc not ready"
995
+ };
996
+ try {
997
+ const res = await rpcCallFn(rpcEndpoint("rotationRemove"), { index });
998
+ if (res && res.ok === true) return res.value ?? {
999
+ ok: false,
1000
+ error: "no value"
1001
+ };
1002
+ return {
1003
+ ok: false,
1004
+ error: res?.error?.message ?? "request failed"
1005
+ };
1006
+ } catch (e) {
1007
+ return {
1008
+ ok: false,
1009
+ error: e instanceof Error ? e.message : String(e)
1010
+ };
1011
+ }
1012
+ }
1013
+ /** Activate a rotation item: the server copies its bytes into the wallpaper
1014
+ * slot and returns the serve URL for immediate display. */
1015
+ async function rotationActivate(index) {
1016
+ if (!rpcCallFn) return {
1017
+ ok: false,
1018
+ error: "rpc not ready"
1019
+ };
1020
+ try {
1021
+ const res = await rpcCallFn(rpcEndpoint("rotationSet"), { index });
1022
+ if (res && res.ok === true) return res.value ?? {
1023
+ ok: false,
1024
+ error: "no value"
1025
+ };
1026
+ return {
1027
+ ok: false,
1028
+ error: res?.error?.message ?? "request failed"
1029
+ };
1030
+ } catch (e) {
1031
+ return {
1032
+ ok: false,
1033
+ error: e instanceof Error ? e.message : String(e)
1034
+ };
1035
+ }
1036
+ }
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
+ }
621
1119
  //#endregion
622
1120
  //#region src/client/utils/color.ts
623
1121
  function hsvToHsl(h, s, v) {
@@ -639,20 +1137,23 @@ window.__ModuleLoader__.load({
639
1137
  let tokensCacheKey = "";
640
1138
  let tokensCache = null;
641
1139
  /**
642
- * Memoized token generation: the same (hue, sat, lit) input always yields the
643
- * same token set, and applyWp / applyCustomTokens / applySettingsOverrides call
644
- * this repeatedly (slider drags, viewport re-applies), so cache the last result
645
- * and skip the 30+ hsl() string builds when nothing changed.
1140
+ * Memoized token generation: the same (hue, sat, lit, scheme) input always
1141
+ * yields the same token set, and applyWp / applyCustomTokens /
1142
+ * applySettingsOverrides call this repeatedly (slider drags, viewport
1143
+ * re-applies), so cache the last result and skip the 30+ hsl() string builds
1144
+ * when nothing changed. The optional scheme forces the palette direction
1145
+ * (light/dark) independently of the color's own lightness.
646
1146
  */
647
- function genTokens(hue, sat, lit) {
648
- const key = `${hue}|${sat}|${lit}`;
1147
+ function genTokens(hue, sat, lit, scheme) {
1148
+ const key = `${hue}|${sat}|${lit}|${scheme ?? "auto"}`;
649
1149
  if (tokensCacheKey === key && tokensCache) return tokensCache;
650
1150
  tokensCacheKey = key;
651
- tokensCache = buildTokens(hue, sat, lit);
1151
+ tokensCache = buildTokens(hue, sat, lit, scheme);
652
1152
  return tokensCache;
653
1153
  }
654
- function buildTokens(hue, sat, lit) {
655
- const dark = lit < .55;
1154
+ function buildTokens(hue, sat, lit, scheme) {
1155
+ const dark = scheme !== void 0 ? scheme === "dark" : lit < .55;
1156
+ if (scheme !== void 0 && lit < .55 !== dark) lit = dark ? Math.min(.44, Math.max(.14, 1 - lit)) : Math.max(.6, Math.min(.88, 1 - lit));
656
1157
  const h = (d) => ((hue + d) % 360 + 360) % 360;
657
1158
  const s = (d) => Math.max(0, Math.min(1, sat + d));
658
1159
  const l = (d) => Math.max(0, Math.min(1, lit + d));
@@ -822,124 +1323,119 @@ window.__ModuleLoader__.load({
822
1323
  return rgbToHsl(b.r / b.count, b.g / b.count, b.b / b.count);
823
1324
  }
824
1325
  function extractWallpaperPalette(dataUrl, bgState) {
825
- return new Promise((resolve) => {
826
- const img = new Image();
827
- img.onerror = () => resolve(null);
828
- img.onload = () => {
829
- try {
830
- const iw = img.naturalWidth || img.width;
831
- const ih = img.naturalHeight || img.height;
832
- const bg = bgState;
833
- let sx = 0, sy = 0, sw = iw, sh = ih;
834
- if (bg.iw === iw && bg.ih === ih && bg.iw > 0) {
835
- const fit = Math.min(window.innerWidth / iw, window.innerHeight / ih);
836
- const w = iw * fit * bg.zoom;
837
- const h = ih * fit * bg.zoom;
838
- sx = Math.max(0, bg.x * window.innerWidth - w / 2);
839
- sy = Math.max(0, bg.y * window.innerHeight - h / 2);
840
- sw = Math.min(iw - sx, w);
841
- sh = Math.min(ih - sy, h);
842
- if (sw <= 0 || sh <= 0) {
843
- sx = 0;
844
- sy = 0;
845
- sw = iw;
846
- sh = ih;
847
- }
848
- }
849
- const c = document.createElement("canvas");
850
- c.width = EXTRACT_SIDE;
851
- c.height = EXTRACT_SIDE;
852
- const g = c.getContext("2d", { willReadFrequently: true });
853
- g.drawImage(img, sx, sy, sw, sh, 0, 0, EXTRACT_SIDE, EXTRACT_SIDE);
854
- const px = g.getImageData(0, 0, EXTRACT_SIDE, EXTRACT_SIDE).data;
855
- const counts = /* @__PURE__ */ new Uint32Array(4096);
856
- const sums = /* @__PURE__ */ new Float64Array(12288);
857
- let totalLum = 0;
858
- let sampled = 0;
859
- for (let i = 0; i < px.length; i += 4) {
860
- const r = px[i], gg = px[i + 1], b = px[i + 2];
861
- const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
862
- const v = max / 255;
863
- const s = max === 0 ? 0 : (max - min) / max;
864
- totalLum += v;
865
- sampled++;
866
- if (s < .08 || v < .12 || v > .97) continue;
867
- const key = r >> 4 << 8 | gg >> 4 << 4 | b >> 4;
868
- counts[key]++;
869
- sums[key * 3] += r;
870
- sums[key * 3 + 1] += gg;
871
- sums[key * 3 + 2] += b;
872
- }
873
- const buckets = [];
874
- for (let k = 0; k < 4096; k++) {
875
- if (counts[k] === 0) continue;
876
- buckets.push({
877
- count: counts[k],
878
- r: sums[k * 3],
879
- g: sums[k * 3 + 1],
880
- b: sums[k * 3 + 2],
881
- 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
882
- });
883
- }
884
- if (buckets.length === 0) {
885
- resolve(null);
886
- 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;
887
1348
  }
888
- buckets.sort((a, b) => b.count * (.5 + b.s) - a.count * (.5 + a.s));
889
- const primary = bucketsToHsl(buckets[0]);
890
- const primaryHue = primary[0];
891
- let secondary = primary;
892
- for (const b of buckets.slice(1)) {
893
- const [h] = bucketsToHsl(b);
894
- if (Math.abs((h - primaryHue + 540) % 360 - 180) > 30) {
895
- secondary = bucketsToHsl(b);
896
- break;
897
- }
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;
898
1395
  }
899
- let tertiary = [
900
- (primaryHue + 180) % 360,
901
- Math.min(.7, primary[1]),
902
- Math.min(.7, primary[2])
903
- ];
904
- for (const b of buckets.slice(1)) {
905
- const [h] = bucketsToHsl(b);
906
- const sep = Math.abs((h - primaryHue + 540) % 360 - 180);
907
- if (sep > 90 && sep < 150) {
908
- tertiary = bucketsToHsl(b);
909
- break;
910
- }
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;
911
1408
  }
912
- const surface = [
913
- primaryHue,
914
- Math.min(.06, primary[1] * .3),
915
- primary[2]
916
- ];
917
- const luminance = sampled > 0 ? totalLum / sampled : primary[2];
918
- primary[2] = luminance < .5 ? Math.min(.44, Math.max(.2, primary[2])) : Math.max(.6, Math.min(.82, primary[2]));
919
- resolve({
920
- primary: [
921
- primary[0],
922
- Math.min(.9, Math.max(.15, primary[1])),
923
- primary[2]
924
- ],
925
- secondary: [
926
- secondary[0],
927
- Math.min(.85, Math.max(.2, secondary[1])),
928
- Math.min(.75, Math.max(.35, secondary[2]))
929
- ],
930
- tertiary: [
931
- tertiary[0],
932
- Math.min(.8, Math.max(.2, tertiary[1])),
933
- Math.min(.7, Math.max(.35, tertiary[2]))
934
- ],
935
- surface,
936
- luminance
937
- });
938
- } catch {
939
- resolve(null);
940
1409
  }
941
- };
942
- 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
+ }
943
1439
  });
944
1440
  }
945
1441
  /** Backward-compatible single-color extraction: returns the primary HSL. */
@@ -952,26 +1448,22 @@ window.__ModuleLoader__.load({
952
1448
  * reads dark (use white fonts), false when light (use black fonts), or null
953
1449
  * when the frame cannot be decoded. */
954
1450
  function analyzeFrameDark(dataUrl) {
955
- return new Promise((resolve) => {
956
- const img = new Image();
957
- img.onerror = () => resolve(null);
958
- img.onload = () => {
959
- try {
960
- const c = document.createElement("canvas");
961
- c.width = ANALYZE_SIDE;
962
- c.height = ANALYZE_SIDE;
963
- const g = c.getContext("2d", { willReadFrequently: true });
964
- g.drawImage(img, 0, 0, ANALYZE_SIDE, ANALYZE_SIDE);
965
- const px = g.getImageData(0, 0, ANALYZE_SIDE, ANALYZE_SIDE).data;
966
- let lum = 0;
967
- const count = px.length / 4;
968
- for (let i = 0; i < px.length; i += 4) lum += .2126 * px[i] + .7152 * px[i + 1] + .0722 * px[i + 2];
969
- resolve(lum / count / 255 < .5);
970
- } catch {
971
- resolve(null);
972
- }
973
- };
974
- 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
+ }
975
1467
  });
976
1468
  }
977
1469
  /** HSL (h 0-360, s/l 0-1) → RGB (0-255 integers). */
@@ -1013,20 +1505,42 @@ window.__ModuleLoader__.load({
1013
1505
  function newRaf(canvas, draw) {
1014
1506
  canvas.dataset.dshAnyCanvas = "1";
1015
1507
  let running = true;
1508
+ let paused = false;
1509
+ let rafId = 0;
1016
1510
  let last = 0;
1017
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
+ };
1018
1519
  const loop = (ts) => {
1019
- if (!running) return;
1520
+ if (!running || paused) return;
1020
1521
  if (ts - last >= FRAME_MS) {
1021
1522
  last = ts;
1022
1523
  draw();
1023
1524
  }
1024
- 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
+ }
1025
1543
  };
1026
- requestAnimationFrame(loop);
1027
- return { stop: () => {
1028
- running = false;
1029
- } };
1030
1544
  }
1031
1545
  function createCanvas() {
1032
1546
  const c = document.createElement("canvas");
@@ -1041,7 +1555,6 @@ window.__ModuleLoader__.load({
1041
1555
  };
1042
1556
  }
1043
1557
  function fitLiveCanvas(c) {
1044
- if (c.dataset.dshAnyStatic === "1") return;
1045
1558
  const { w, h } = liveSize();
1046
1559
  if (c.width !== w || c.height !== h) {
1047
1560
  c.width = w;
@@ -1338,6 +1851,43 @@ window.__ModuleLoader__.load({
1338
1851
  col = hueRotate(col, u_seed * 6.28318530718);
1339
1852
  gl_FragColor = vec4(col, 1.0);
1340
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
+ }
1341
1891
  `;
1342
1892
  return common + `
1343
1893
  void main() {
@@ -1361,44 +1911,52 @@ window.__ModuleLoader__.load({
1361
1911
  function createPatternBg(params, canvas) {
1362
1912
  if (params.preset === "waves") return createWaves(params, canvas);
1363
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);
1364
1917
  return createDots(params, canvas);
1365
1918
  }
1366
- function createDots(params, canvas) {
1919
+ function createRain(params, canvas) {
1367
1920
  const c = canvas ?? createCanvas();
1368
1921
  fitLiveCanvas(c);
1369
1922
  const g = c.getContext("2d", { alpha: false });
1370
1923
  const rng = createRng(params.seed);
1371
1924
  const dark = rng() < .5;
1372
- const baseHue = Math.round(rng() * 360);
1373
- const spacing = Math.max(26, 150 * params.scale / (.25 + params.density));
1374
- const dots = [];
1375
- for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) dots.push({
1376
- cx: x / 7,
1377
- cy: y / 7,
1378
- r: spacing * .16 * params.scale * (.6 + rng() * .7),
1379
- hue: (baseHue + rng() * 80) % 360,
1380
- sat: Math.round(35 + rng() * 55),
1381
- lit: dark ? Math.round(30 + rng() * 35) : Math.round(55 + rng() * 30),
1382
- phase: rng() * Math.PI * 2,
1383
- speed: .5 + rng() * 1.2
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
1384
1937
  });
1385
1938
  let t = 0;
1386
1939
  const draw = () => {
1387
1940
  fitLiveCanvas(c);
1388
1941
  const w = c.width, h = c.height;
1389
- g.fillStyle = dark ? "#0a0b0d" : "#f6f7f9";
1942
+ g.fillStyle = dark ? "#07080c" : "#f4f6f9";
1390
1943
  g.fillRect(0, 0, w, h);
1391
- for (const d of dots) {
1392
- const pulse = .75 + .35 * Math.sin(t * d.speed + d.phase);
1393
- const r = Math.max(1, d.r * pulse);
1394
- const x = d.cx * w + d.r * .3 * Math.sin(t * d.speed * .5 + d.phase);
1395
- const y = d.cy * h + d.r * .3 * Math.cos(t * d.speed * .7 + d.phase);
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;
1396
1954
  g.beginPath();
1397
- g.arc(x, y, r, 0, Math.PI * 2);
1398
- g.fillStyle = `hsla(${d.hue},${d.sat}%,${d.lit}%,${(.18 + .25 * pulse).toFixed(2)})`;
1399
- g.fill();
1955
+ g.moveTo(x, y0);
1956
+ g.lineTo(x, y1);
1957
+ g.stroke();
1400
1958
  }
1401
- t += .032;
1959
+ t += .016;
1402
1960
  };
1403
1961
  return {
1404
1962
  canvas: c,
@@ -1406,15 +1964,153 @@ window.__ModuleLoader__.load({
1406
1964
  snapshot: () => c.toDataURL("image/jpeg", .94)
1407
1965
  };
1408
1966
  }
1409
- function createWaves(params, canvas) {
1967
+ function createContour(params, canvas) {
1410
1968
  const c = canvas ?? createCanvas();
1411
1969
  fitLiveCanvas(c);
1412
1970
  const g = c.getContext("2d", { alpha: false });
1413
- const rng = createRng(params.seed);
1971
+ const noise = createNoise(params.seed);
1972
+ const rng = createRng(params.seed + 1013);
1414
1973
  const dark = rng() < .5;
1415
1974
  const hue = Math.round(rng() * 360);
1416
- const layers = Math.round(5 + params.density * 8);
1417
- const waves = [];
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
+ }
2062
+ function createDots(params, canvas) {
2063
+ const c = canvas ?? createCanvas();
2064
+ fitLiveCanvas(c);
2065
+ const g = c.getContext("2d", { alpha: false });
2066
+ const rng = createRng(params.seed);
2067
+ const dark = rng() < .5;
2068
+ const baseHue = Math.round(rng() * 360);
2069
+ const spacing = Math.max(26, 150 * params.scale / (.25 + params.density));
2070
+ const dots = [];
2071
+ for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) dots.push({
2072
+ cx: x / 7,
2073
+ cy: y / 7,
2074
+ r: spacing * .16 * params.scale * (.6 + rng() * .7),
2075
+ hue: (baseHue + rng() * 80) % 360,
2076
+ sat: Math.round(35 + rng() * 55),
2077
+ lit: dark ? Math.round(30 + rng() * 35) : Math.round(55 + rng() * 30),
2078
+ phase: rng() * Math.PI * 2,
2079
+ speed: .5 + rng() * 1.2
2080
+ });
2081
+ let t = 0;
2082
+ const draw = () => {
2083
+ fitLiveCanvas(c);
2084
+ const w = c.width, h = c.height;
2085
+ g.fillStyle = dark ? "#0a0b0d" : "#f6f7f9";
2086
+ g.fillRect(0, 0, w, h);
2087
+ for (const d of dots) {
2088
+ const pulse = .75 + .35 * Math.sin(t * d.speed + d.phase);
2089
+ const r = Math.max(1, d.r * pulse);
2090
+ const x = d.cx * w + d.r * .3 * Math.sin(t * d.speed * .5 + d.phase);
2091
+ const y = d.cy * h + d.r * .3 * Math.cos(t * d.speed * .7 + d.phase);
2092
+ g.beginPath();
2093
+ g.arc(x, y, r, 0, Math.PI * 2);
2094
+ g.fillStyle = `hsla(${d.hue},${d.sat}%,${d.lit}%,${(.18 + .25 * pulse).toFixed(2)})`;
2095
+ g.fill();
2096
+ }
2097
+ t += .032;
2098
+ };
2099
+ return {
2100
+ canvas: c,
2101
+ ...newRaf(c, draw),
2102
+ snapshot: () => c.toDataURL("image/jpeg", .94)
2103
+ };
2104
+ }
2105
+ function createWaves(params, canvas) {
2106
+ const c = canvas ?? createCanvas();
2107
+ fitLiveCanvas(c);
2108
+ const g = c.getContext("2d", { alpha: false });
2109
+ const rng = createRng(params.seed);
2110
+ const dark = rng() < .5;
2111
+ const hue = Math.round(rng() * 360);
2112
+ const layers = Math.round(5 + params.density * 8);
2113
+ const waves = [];
1418
2114
  for (let i = 0; i < layers; i++) waves.push({
1419
2115
  yBase: .25 + i / layers * .55,
1420
2116
  amp: (25 + rng() * 45) * params.scale,
@@ -1567,6 +2263,14 @@ window.__ModuleLoader__.load({
1567
2263
  let wpController = null;
1568
2264
  let snapshotListener = null;
1569
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
+ }
1570
2274
  function clearDynamicBg() {
1571
2275
  wpController?.stop();
1572
2276
  wpController?.canvas.remove();
@@ -1589,10 +2293,20 @@ window.__ModuleLoader__.load({
1589
2293
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1590
2294
  appliedTokenNames = [];
1591
2295
  }
1592
- /** Label tokens flipped by the generated-background brightness verdict. The
1593
- * faint tiers (caption/dimmed) are deliberately NOT flipped: they back
1594
- * placeholder/hint text, which must stay visibly weaker than real input even
1595
- * when the wallpaper brightness flips the main label direction. */
2296
+ /** Drop every applied custom token and forget the token fingerprint, so a
2297
+ * later color-less profile (system theme) leaves no stale rule behind. */
2298
+ function clearThemeTokens() {
2299
+ clearCustomTokens();
2300
+ baseTokenKey = "";
2301
+ lastBgKey = "";
2302
+ document.body.removeAttribute("data-ds-dark-theme");
2303
+ document.body.style.removeProperty("color-scheme");
2304
+ }
2305
+ /** Label tokens flipped by the background brightness verdict. The faint tiers
2306
+ * (caption/dimmed) are deliberately NOT flipped: they back placeholder/hint
2307
+ * text, which must stay visibly weaker than real input even when the wallpaper
2308
+ * brightness flips the main label direction. Exported so the skin registration
2309
+ * (index) can flip fonts through the host theme service too. */
1596
2310
  const LABEL_TOKENS = [
1597
2311
  "--dsw-alias-label-primary",
1598
2312
  "--dsw-alias-label-secondary",
@@ -1645,19 +2359,41 @@ window.__ModuleLoader__.load({
1645
2359
  });
1646
2360
  }
1647
2361
  let lastBgKey = "";
2362
+ /** Palette source for the token rule: the picked color; a forced scheme
2363
+ * without a picked color builds a neutral near-gray palette in that
2364
+ * direction; auto without a color keeps the host's own palette (null — only
2365
+ * the label direction gets asserted). The palette direction follows the
2366
+ * color's own lightness (rColorScheme), not the wallpaper verdict, so the
2367
+ * surfaces keep contrasting with the fonts. */
2368
+ function paletteTokens() {
2369
+ const scheme = rColorScheme();
2370
+ if (rHasColor()) {
2371
+ const [h, s, l] = rColor();
2372
+ return genTokens(h, s, l, scheme).tokens;
2373
+ }
2374
+ if (rSchemeOverride() !== "auto") return genTokens(220, .04, scheme === "dark" ? .14 : .92, scheme).tokens;
2375
+ return null;
2376
+ }
1648
2377
  function applyCustomTokensNow(ops) {
2378
+ const hasColor = rHasColor();
2379
+ const override = rSchemeOverride();
1649
2380
  const [h, s, l] = rColor();
1650
- let { tokens } = genTokens(h, s, l);
1651
- try {
1652
- const dark = rBgDark();
1653
- if (dark !== null) {
1654
- tokens = { ...tokens };
1655
- const font = dark ? "#fff" : "#000";
2381
+ const scheme = rScheme();
2382
+ const verdict = rBgDark();
2383
+ const palette = paletteTokens();
2384
+ const tokens = { ...palette };
2385
+ if (override === "auto") {
2386
+ const fontDark = hasColor ? rColorScheme() === "dark" : verdict;
2387
+ if (fontDark !== null && fontDark !== void 0) {
2388
+ const font = fontDark ? "#fff" : "#000";
1656
2389
  for (const name of LABEL_TOKENS) tokens[name] = font;
1657
2390
  }
1658
- const forceDark = dark ?? l < .55;
1659
- if (`${h}|${s}|${l}|${dark}` !== baseTokenKey) {
1660
- baseTokenKey = `${h}|${s}|${l}|${dark}`;
2391
+ }
2392
+ try {
2393
+ const forceDark = scheme === "dark";
2394
+ const key = `${h}|${s}|${l}|${verdict}|${scheme}|${hasColor}`;
2395
+ if (key !== baseTokenKey) {
2396
+ baseTokenKey = key;
1661
2397
  if (forceDark) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
1662
2398
  else document.body.removeAttribute("data-ds-dark-theme");
1663
2399
  const decls = [`color-scheme:${forceDark ? "dark" : "light"}`];
@@ -1669,6 +2405,7 @@ window.__ModuleLoader__.load({
1669
2405
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1670
2406
  appliedTokenNames = Object.keys(tokens);
1671
2407
  }
2408
+ if (palette === null) return;
1672
2409
  const root = document.documentElement;
1673
2410
  for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) root.style.setProperty(OPACITY_VARS[name], toRgba(tokens[name] ?? "#000", ops[g.part]));
1674
2411
  root.style.setProperty("--dsh-any-op-menu-cordis", toRgba(tokens["--dsw-specific-menu"] ?? "#000", ops.input));
@@ -1686,9 +2423,17 @@ window.__ModuleLoader__.load({
1686
2423
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-input-blur", `blur(${px}px)`);
1687
2424
  else document.documentElement.style.removeProperty("--dsh-any-input-blur");
1688
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
+ }
1689
2434
  function applySettingsOverrides(op) {
1690
2435
  const [h, s, l] = rColor();
1691
- const tokens = genTokens(h, s, l).tokens;
2436
+ const tokens = genTokens(h, s, l, rColorScheme()).tokens;
1692
2437
  const layer1 = tokens["--dsw-alias-bg-layer-1"];
1693
2438
  const layer2 = tokens["--dsw-alias-bg-layer-2"];
1694
2439
  const layer3 = tokens["--dsw-alias-bg-layer-3"];
@@ -1700,7 +2445,7 @@ window.__ModuleLoader__.load({
1700
2445
  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)}";
1701
2446
  function applyTrajectoryOverrides(op) {
1702
2447
  const [h, s, l] = rColor();
1703
- const tokens = genTokens(h, s, l).tokens;
2448
+ const tokens = genTokens(h, s, l, rColorScheme()).tokens;
1704
2449
  const layer1 = tokens["--dsw-alias-bg-layer-1"];
1705
2450
  const layer2 = tokens["--dsw-alias-bg-layer-2"];
1706
2451
  const layer3 = tokens["--dsw-alias-bg-layer-3"];
@@ -1708,6 +2453,14 @@ window.__ModuleLoader__.load({
1708
2453
  if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-2", toRgba(layer2, op));
1709
2454
  if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-3", toRgba(layer3, op));
1710
2455
  }
2456
+ /** Register a callback fired when a wallpaper-extracted theme color is
2457
+ * adopted by the auto-adaptation path, so the section can re-register the
2458
+ * host skin, sync the editor UI and persist the pick (wallpaper.ts cannot
2459
+ * do those itself — they live in the section). */
2460
+ let colorAdoptedListener = null;
2461
+ function onColorAdopted(cb) {
2462
+ colorAdoptedListener = cb;
2463
+ }
1711
2464
  /** Apply the theme color: use the saved pick directly, or fall back to
1712
2465
  * extracting a dominant color from the current wallpaper. */
1713
2466
  function applyThemeColor() {
@@ -1716,11 +2469,16 @@ window.__ModuleLoader__.load({
1716
2469
  return;
1717
2470
  }
1718
2471
  const url = rWp();
1719
- if (url) extractWallpaperColor(url, cfg.backgroundType === "video" ? rVideoBgState() : rBgState()).then((hsl) => {
1720
- if (hsl) cfg.color = hsl;
2472
+ if (url) {
1721
2473
  applyWp();
1722
- });
1723
- 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();
1724
2482
  }
1725
2483
  /** Switch the background source type. For generated types a new live canvas is
1726
2484
  * attached to the wallpaper layer and a snapshot is kept for the store/preview. */
@@ -1782,7 +2540,7 @@ window.__ModuleLoader__.load({
1782
2540
  setBgDark(null);
1783
2541
  analyzeFrameDark(frame).then((dark) => {
1784
2542
  if (dark === null || wpController !== controller) return;
1785
- setBgDark(dark);
2543
+ applyVerdict(dark);
1786
2544
  applyCustomTokens(rOps());
1787
2545
  });
1788
2546
  });
@@ -1848,11 +2606,11 @@ window.__ModuleLoader__.load({
1848
2606
  * reducing the main-bg opacity stacked a second alpha onto the sidebar; moving
1849
2607
  * the alpha onto the columns keeps the sidebar owned by its own slider. */
1850
2608
  function applyPartOpacities(ops) {
1851
- if (!(rHasColor() || rBgDark() !== null)) return;
2609
+ const tokens = paletteTokens();
2610
+ if (tokens === null) return;
1852
2611
  discoverParts();
1853
2612
  if (frameEl === null) return;
1854
- const [h, s, l] = rColor();
1855
- const base = genTokens(h, s, l).tokens["--dsw-alias-bg-base"];
2613
+ const base = tokens["--dsw-alias-bg-base"];
1856
2614
  frameEl.style.background = "transparent";
1857
2615
  if (centerEl !== null) centerEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
1858
2616
  if (detailsEl !== null) detailsEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
@@ -1872,6 +2630,7 @@ window.__ModuleLoader__.load({
1872
2630
  setBlur(sidebarEl, blurs.sidebar);
1873
2631
  setBlur(centerEl, blurs.bg);
1874
2632
  setBlur(detailsEl, blurs.bg);
2633
+ applyBgBlurGlobal(blurs.bg);
1875
2634
  applyCardPanelsBlur(blurs.card);
1876
2635
  applySettingsBlur(blurs.settings);
1877
2636
  applyInputBlur(blurs.input);
@@ -1899,6 +2658,7 @@ window.__ModuleLoader__.load({
1899
2658
  if (part === "bg") {
1900
2659
  setBlur(centerEl, v);
1901
2660
  setBlur(detailsEl, v);
2661
+ applyBgBlurGlobal(v);
1902
2662
  } else setBlur(sidebarEl, v);
1903
2663
  }
1904
2664
  const VIEW_CARDS = [{
@@ -2071,7 +2831,7 @@ window.__ModuleLoader__.load({
2071
2831
  discoverParts();
2072
2832
  if (centerEl === null) return;
2073
2833
  const [h, s, l] = rColor();
2074
- const surface = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-1"];
2834
+ const surface = genTokens(h, s, l, rColorScheme()).tokens["--dsw-alias-bg-layer-1"];
2075
2835
  VIEW_CARDS.forEach((spec, i) => {
2076
2836
  const target = discoverViewTarget(i, spec);
2077
2837
  if (target === null) return;
@@ -2114,8 +2874,7 @@ window.__ModuleLoader__.load({
2114
2874
  let themeObserver = null;
2115
2875
  let themeRaf = 0;
2116
2876
  function reassertScheme() {
2117
- const [, , l] = rColor();
2118
- if (rBgDark() ?? l < .55) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
2877
+ if (rScheme() === "dark") document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
2119
2878
  else document.body.removeAttribute("data-ds-dark-theme");
2120
2879
  applyCustomTokens(rOps());
2121
2880
  }
@@ -2126,7 +2885,7 @@ window.__ModuleLoader__.load({
2126
2885
  if (themeObserver !== null || typeof MutationObserver === "undefined") return () => void 0;
2127
2886
  themeObserver = new MutationObserver(() => {
2128
2887
  if (document.body.getAttribute("data-ds-dark-theme") === "dsh-any-background") return;
2129
- if (!(rHasColor() || rBgDark() !== null)) return;
2888
+ if (!(rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto")) return;
2130
2889
  if (themeRaf !== 0) return;
2131
2890
  themeRaf = requestAnimationFrame(() => {
2132
2891
  themeRaf = 0;
@@ -2169,17 +2928,18 @@ window.__ModuleLoader__.load({
2169
2928
  cb(imgNat.w, imgNat.h);
2170
2929
  return;
2171
2930
  }
2172
- const img = new Image();
2173
- img.onload = () => {
2931
+ loadImage(url).then((img) => {
2932
+ if (!img) {
2933
+ cb(0, 0);
2934
+ return;
2935
+ }
2174
2936
  imgNat = {
2175
2937
  url,
2176
2938
  w: img.naturalWidth,
2177
2939
  h: img.naturalHeight
2178
2940
  };
2179
2941
  cb(img.naturalWidth, img.naturalHeight);
2180
- };
2181
- img.onerror = () => cb(0, 0);
2182
- img.src = url;
2942
+ });
2183
2943
  }
2184
2944
  const DRAG_MAX_SIDE = 720;
2185
2945
  let lowResUrl = null;
@@ -2190,8 +2950,11 @@ window.__ModuleLoader__.load({
2190
2950
  cb(lowResUrl);
2191
2951
  return;
2192
2952
  }
2193
- const img = new Image();
2194
- img.onload = () => {
2953
+ loadImage(url).then((img) => {
2954
+ if (!img) {
2955
+ cb(null);
2956
+ return;
2957
+ }
2195
2958
  const k = Math.min(1, DRAG_MAX_SIDE / Math.max(img.naturalWidth, img.naturalHeight));
2196
2959
  if (k >= 1) {
2197
2960
  lowResFor = url;
@@ -2214,9 +2977,7 @@ window.__ModuleLoader__.load({
2214
2977
  lowResFor = url;
2215
2978
  lowResUrl = low;
2216
2979
  cb(low);
2217
- };
2218
- img.onerror = () => cb(null);
2219
- img.src = url;
2980
+ });
2220
2981
  }
2221
2982
  function setDragLow(on) {
2222
2983
  if (cfg.backgroundType !== "image" || on === dragLow || !wpEl) return;
@@ -2253,6 +3014,40 @@ window.__ModuleLoader__.load({
2253
3014
  if (dragLow) setDragLow(false);
2254
3015
  };
2255
3016
  }
3017
+ let wpVerdict = null;
3018
+ let verdictListener = null;
3019
+ let verdictGen = 0;
3020
+ /** Register a callback fired when the background brightness verdict CHANGES
3021
+ * (a new wallpaper was analyzed, a generated bg regenerated), so the skin can
3022
+ * be re-registered through the host theme service. */
3023
+ function onVerdictApplied(cb) {
3024
+ verdictListener = cb;
3025
+ }
3026
+ function applyVerdict(dark) {
3027
+ if (rBgDark() === dark) return;
3028
+ setBgDark(dark);
3029
+ if (dark !== null) verdictListener?.();
3030
+ }
3031
+ function updateWpVerdict(url) {
3032
+ const gen = ++verdictGen;
3033
+ if (url === null) {
3034
+ applyVerdict(null);
3035
+ return;
3036
+ }
3037
+ if (wpVerdict !== null && wpVerdict.url === url) {
3038
+ applyVerdict(wpVerdict.dark);
3039
+ return;
3040
+ }
3041
+ analyzeFrameDark(url).then((dark) => {
3042
+ if (dark === null || gen !== verdictGen) return;
3043
+ wpVerdict = {
3044
+ url,
3045
+ dark
3046
+ };
3047
+ applyVerdict(dark);
3048
+ applyCustomTokens(rOps());
3049
+ });
3050
+ }
2256
3051
  function applyImageWp(url) {
2257
3052
  clearDynamicBg();
2258
3053
  clearVideoEl();
@@ -2299,6 +3094,7 @@ window.__ModuleLoader__.load({
2299
3094
  }
2300
3095
  captureLowRes(url, () => void 0);
2301
3096
  applyWpEffects();
3097
+ updateWpVerdict(url);
2302
3098
  }
2303
3099
  /** Video wallpaper: a muted looping <video> inside the wallpaper layer.
2304
3100
  * Placement modes map onto object-fit (tile has no video equivalent and
@@ -2335,6 +3131,7 @@ window.__ModuleLoader__.load({
2335
3131
  videoEl.style.objectFit = mode === "stretch" ? "fill" : mode === "fill" || mode === "tile" ? "cover" : "contain";
2336
3132
  }
2337
3133
  applyWpEffects();
3134
+ updateWpVerdict(rWp());
2338
3135
  }
2339
3136
  function applyWpEffects() {
2340
3137
  if (!wpEl) return;
@@ -2368,8 +3165,11 @@ window.__ModuleLoader__.load({
2368
3165
  clearVideoEl();
2369
3166
  wpEl?.remove();
2370
3167
  wpEl = null;
3168
+ wpVerdict = null;
3169
+ setBgDark(null);
2371
3170
  }
2372
- if (rHasColor() || rBgDark() !== null) applyCustomTokens(rOps());
3171
+ if (rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto") applyCustomTokens(rOps());
3172
+ else if (!url) clearThemeTokens();
2373
3173
  if (rHasColor()) {
2374
3174
  applySettingsOverrides(rSop());
2375
3175
  applyTrajectoryOverrides(rTrajectoryOpacity());
@@ -2381,6 +3181,7 @@ window.__ModuleLoader__.load({
2381
3181
  clearVideoEl();
2382
3182
  disposeVideoObjectUrl();
2383
3183
  setBgDark(null);
3184
+ wpVerdict = null;
2384
3185
  wpEl?.remove();
2385
3186
  wpEl = null;
2386
3187
  clearCustomTokens();
@@ -2400,6 +3201,7 @@ window.__ModuleLoader__.load({
2400
3201
  document.documentElement.style.removeProperty("--dsh-any-blur-settings");
2401
3202
  document.documentElement.style.removeProperty("--dsh-any-blur-card-panels");
2402
3203
  document.documentElement.style.removeProperty("--dsh-any-input-blur");
3204
+ document.documentElement.style.removeProperty("--dsh-any-part-blur-global");
2403
3205
  for (const v of Object.values(OPACITY_VARS)) document.documentElement.style.removeProperty(v);
2404
3206
  baseTokenKey = "";
2405
3207
  lastBgKey = "";
@@ -2559,6 +3361,7 @@ window.__ModuleLoader__.load({
2559
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}
2560
3362
  .dab-urlinput::placeholder{color:var(--dsw-alias-label-quaternary)}
2561
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}
2562
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}
2563
3366
  .dab-swatch-lg:hover{transform:scale(1.02)}
2564
3367
 
@@ -2569,11 +3372,19 @@ window.__ModuleLoader__.load({
2569
3372
  .dab-btn:disabled{opacity:.5;cursor:not-allowed}
2570
3373
  .dab-btn-primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);border-color:transparent}
2571
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)}
2572
3377
  .dab-btn-ghost{background:transparent;border-color:transparent;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
2573
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}
2574
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}
2575
3386
  .dab-chip-row{display:flex;flex-wrap:wrap;gap:8px}
2576
- .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}
2577
3388
  .dab-chip:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
2578
3389
  .dab-chip.is-active{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);border-color:transparent}
2579
3390
 
@@ -2623,7 +3434,7 @@ window.__ModuleLoader__.load({
2623
3434
 
2624
3435
  /* ── generated background type cards ─────────────────────────────────────── */
2625
3436
  .dab-types{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
2626
- .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)}
2627
3438
  .dab-type:hover{transform:translateY(-2px)}
2628
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)}
2629
3440
  .dab-type-thumb{height:62px;border-radius:9px;overflow:hidden;position:relative}
@@ -2651,6 +3462,52 @@ window.__ModuleLoader__.load({
2651
3462
  .dab-footer{margin-top:6px;padding:14px 4px 0;border-top:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dsw-alias-label-tertiary)}
2652
3463
  .dab-footer-mono{font-family:var(--dab-mono);letter-spacing:.02em}
2653
3464
 
3465
+ /* ── presets / profiles / schedule / rotation ────────────────────────────── */
3466
+ .dab-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
3467
+ .dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
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)}
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)}
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)}
3471
+ .dab-preset-name{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary)}
3472
+ .dab-preset-desc{font-size:10.5px;line-height:1.45;color:var(--dsw-alias-label-tertiary)}
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))}
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)}
3478
+ .dab-profile-meta{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
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}
3480
+ .dab-profile-sub{font-family:var(--dab-mono);font-size:10px;color:var(--dsw-alias-label-tertiary)}
3481
+ .dab-btn-confirm{background:var(--dsw-alias-state-error-primary)!important;color:#fff!important;border-color:transparent!important}
3482
+ .dab-select{height:32px;padding:0 10px;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-size:12px;outline:none;cursor:pointer;max-width:180px}
3483
+ .dab-select:focus{border-color:var(--dsw-alias-brand-primary)}
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}
3485
+ .dab-timeinput:focus{border-color:var(--dsw-alias-brand-primary)}
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)}
3496
+ .dab-time-row{display:flex;flex-direction:column;gap:9px}
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))}
3498
+ .dab-time-label svg{color:var(--dsw-alias-brand-primary);flex:none}
3499
+ .dab-time-label .dab-select,.dab-time-label .dab-timeinput{flex:1;min-width:0}
3500
+ .dab-thumbstrip{display:flex;flex-wrap:wrap;gap:9px;margin-top:13px}
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}
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)}
3503
+ .dab-thumb img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
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}
3505
+ .dab-thumb:hover .dab-thumb-x{opacity:1}
3506
+ .dab-thumb-x:hover{background:var(--dsw-alias-state-error-primary)}
3507
+ .dab-thumb-add{border-style:dashed;cursor:pointer;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;transition:border-color .22s ease,color .22s ease}
3508
+ .dab-thumb-add:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
3509
+ .dab-chip-sep{width:1px;height:18px;background:var(--dsw-alias-border-l2);margin:0 4px;flex:none}
3510
+
2654
3511
  /* ── toast ───────────────────────────────────────────────────────────────── */
2655
3512
  .dab-toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%);display:flex;align-items:center;gap:8px;height:38px;padding:0 16px;border-radius:99px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2);box-shadow:0 10px 30px -8px rgba(0,0,0,.38);font-size:12.5px;z-index:10001;animation:dab-toast-in .32s cubic-bezier(.22,1,.36,1) both}
2656
3513
  .dab-toast-ok{color:var(--dsw-alias-brand-primary);display:grid;place-items:center}
@@ -2675,6 +3532,9 @@ window.__ModuleLoader__.load({
2675
3532
  @keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
2676
3533
  @keyframes dab-page-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
2677
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}}
2678
3538
  @keyframes dab-orb-in{from{opacity:0;transform:scale(.82)}to{opacity:1;transform:none}}
2679
3539
  @keyframes dab-spin{to{transform:rotate(360deg)}}
2680
3540
  @keyframes dab-rotate{to{transform:rotate(360deg)}}
@@ -2958,6 +3818,40 @@ window.__ModuleLoader__.load({
2958
3818
  rx: "2"
2959
3819
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.6v2M7 8h2" })]
2960
3820
  });
3821
+ const PlusIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3822
+ size,
3823
+ className,
3824
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 3v10M3 8h10" })
3825
+ });
3826
+ const MoonIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3827
+ size,
3828
+ className,
3829
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.2 9.6A5.6 5.6 0 0 1 6.4 2.8a5.6 5.6 0 1 0 6.8 6.8z" })
3830
+ });
3831
+ const ClockIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
3832
+ size,
3833
+ className,
3834
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3835
+ cx: "8",
3836
+ cy: "8",
3837
+ r: "5.8"
3838
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 4.8V8l2.2 1.6" })]
3839
+ });
3840
+ const XIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3841
+ size,
3842
+ className,
3843
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
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
+ });
2961
3855
  //#endregion
2962
3856
  //#region src/client/components/ErrorBoundary.tsx
2963
3857
  /**
@@ -3600,12 +4494,13 @@ window.__ModuleLoader__.load({
3600
4494
  return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
3601
4495
  }
3602
4496
  function ColorPage({ p, notify }) {
3603
- const { t, hue, sat, lit, setColor, extractColor, useStore } = p;
3604
- const store = useStore((s) => s);
3605
- const storeUrl = store.url;
4497
+ const { t, hue, sat, lit, setColor, extractColor, setSchemeOverride, useStore } = p;
4498
+ const storeUrl = useStore((s) => s.url);
4499
+ const schemeOverride = useStore((s) => s.schemeOverride);
4500
+ const color = useStore((s) => s.color);
3606
4501
  const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
3607
4502
  const [extracting, setExtracting] = (0, react.useState)(false);
3608
- const wheel = store.color ?? [
4503
+ const wheel = color ?? [
3609
4504
  hue,
3610
4505
  sat,
3611
4506
  lit
@@ -3748,6 +4643,49 @@ window.__ModuleLoader__.load({
3748
4643
  })
3749
4644
  })]
3750
4645
  }),
4646
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4647
+ className: "dab-card dab-rise",
4648
+ style: { "--d": 4 },
4649
+ children: [
4650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4651
+ className: "dab-swatch-title",
4652
+ children: t("schemeTitle")
4653
+ }),
4654
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4655
+ className: "dab-seg",
4656
+ style: { "--w": "86px" },
4657
+ children: [
4658
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4659
+ className: "dab-seg-thumb",
4660
+ style: { transform: `translateX(${schemeOverride === "light" ? 0 : schemeOverride === "dark" ? 86 : 172}px)` }
4661
+ }),
4662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4663
+ type: "button",
4664
+ className: `dab-seg-item${schemeOverride === "light" ? " is-active" : ""}`,
4665
+ onClick: () => setSchemeOverride("light"),
4666
+ children: t("schemeLight")
4667
+ }),
4668
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4669
+ type: "button",
4670
+ className: `dab-seg-item${schemeOverride === "dark" ? " is-active" : ""}`,
4671
+ onClick: () => setSchemeOverride("dark"),
4672
+ children: t("schemeDark")
4673
+ }),
4674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4675
+ type: "button",
4676
+ className: `dab-seg-item${schemeOverride === "auto" ? " is-active" : ""}`,
4677
+ onClick: () => setSchemeOverride("auto"),
4678
+ children: t("schemeAuto")
4679
+ })
4680
+ ]
4681
+ }),
4682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4683
+ className: "dab-hint",
4684
+ style: { marginTop: 10 },
4685
+ children: t("schemeHint")
4686
+ })
4687
+ ]
4688
+ }),
3751
4689
  pickerOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorPicker, {
3752
4690
  url: storeUrl,
3753
4691
  t,
@@ -3989,20 +4927,6 @@ window.__ModuleLoader__.load({
3989
4927
  })] });
3990
4928
  }
3991
4929
  //#endregion
3992
- //#region src/client/utils/image.ts
3993
- /**
3994
- * Read a chosen image file as a data URL WITHOUT re-encoding: the original
3995
- * pixels are kept as-is (no canvas downscale / JPEG re-compression), so the
3996
- * wallpaper is stored and displayed at full fidelity. The tradeoff is a larger
3997
- * payload over the RPC channel and on disk for big images.
3998
- */
3999
- function readImg(file, cb) {
4000
- const r = new FileReader();
4001
- r.onerror = () => cb(null);
4002
- r.onload = () => cb(r.result);
4003
- r.readAsDataURL(file);
4004
- }
4005
- //#endregion
4006
4930
  //#region src/client/components/BgEditor.tsx
4007
4931
  /** Placement slot for the active background type: video framing lives in its
4008
4932
  * own state so image and video edits never overwrite each other. */
@@ -4218,16 +5142,18 @@ window.__ModuleLoader__.load({
4218
5142
  }
4219
5143
  ];
4220
5144
  function BackgroundPage({ p }) {
4221
- const { t, setWp, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, useStore } = p;
4222
- const store = useStore((s) => s);
4223
- const storeUrl = store.url;
4224
- const backgroundType = store.backgroundType;
4225
- const generatedBg = store.generatedBg;
4226
- const regenerateOnReload = store.regenerateOnReload;
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);
4227
5151
  const fileRef = (0, react.useRef)(null);
5152
+ const rotFileRef = (0, react.useRef)(null);
4228
5153
  const [editorOpen, setEditorOpen] = (0, react.useState)(false);
4229
5154
  const [dragOver, setDragOver] = (0, react.useState)(false);
4230
5155
  const [spinTick, setSpinTick] = (0, react.useState)(0);
5156
+ const [rotBusy, setRotBusy] = (0, react.useState)(false);
4231
5157
  const [urlOpen, setUrlOpen] = (0, react.useState)(false);
4232
5158
  const [urlVal, setUrlVal] = (0, react.useState)("");
4233
5159
  const [urlBusy, setUrlBusy] = (0, react.useState)(false);
@@ -4237,13 +5163,22 @@ window.__ModuleLoader__.load({
4237
5163
  const isStatic = backgroundType === "image" || isVideo;
4238
5164
  const isGenerated = !isStatic;
4239
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
+ ]);
4240
5175
  const onFileSelect = (f) => {
4241
5176
  if (f.type.startsWith("video/")) {
4242
5177
  setVideo(f, f.type);
4243
5178
  return;
4244
5179
  }
4245
- readImg(f, (d) => {
4246
- if (d) setWp(d);
5180
+ uploadWallpaper(f).then((ok) => {
5181
+ if (ok) setWpFromServer(WALLPAPER_SERVE_URL);
4247
5182
  });
4248
5183
  };
4249
5184
  const onDrop = (e) => {
@@ -4261,12 +5196,17 @@ window.__ModuleLoader__.load({
4261
5196
  setUrlBusy(true);
4262
5197
  setUrlErr(null);
4263
5198
  const r = await setWallpaperFromUrl(u);
4264
- setUrlBusy(false);
4265
5199
  if (r.ok) {
4266
- setWp(r.dataUrl ?? null);
5200
+ setWpFromServer(r.wallpaperUrl ?? null);
4267
5201
  setUrlOpen(false);
4268
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"));
4269
5208
  } else setUrlErr(r.error === "invalid url" || r.error === "unsupported scheme" ? t("bgUrlBadHttp") : r.error ?? t("bgUrlFail"));
5209
+ setUrlBusy(false);
4270
5210
  };
4271
5211
  const switchToStatic = () => {
4272
5212
  if (isStatic) return;
@@ -4281,10 +5221,12 @@ window.__ModuleLoader__.load({
4281
5221
  };
4282
5222
  const setGenType = (type) => {
4283
5223
  if (type === activeGenType) return;
5224
+ setPaused(false);
4284
5225
  setBgType(type);
4285
5226
  };
4286
5227
  const ensureGenParams = () => generatedBg ?? defaultParamsFor(activeGenType);
4287
5228
  const updateGenerated = (patch) => {
5229
+ setPaused(false);
4288
5230
  setGeneratedBg({
4289
5231
  ...ensureGenParams(),
4290
5232
  ...patch
@@ -4315,9 +5257,13 @@ window.__ModuleLoader__.load({
4315
5257
  case "aurora": return t("presetAurora");
4316
5258
  case "nebula": return t("presetNebula");
4317
5259
  case "noise": return t("presetNoise");
5260
+ case "starfield": return t("presetStarfield");
4318
5261
  case "dots": return t("presetDots");
4319
5262
  case "waves": return t("presetWaves");
4320
5263
  case "poly": return t("presetPoly");
5264
+ case "rain": return t("presetRain");
5265
+ case "contour": return t("presetContour");
5266
+ case "meta": return t("presetMeta");
4321
5267
  default: return key;
4322
5268
  }
4323
5269
  };
@@ -4372,6 +5318,7 @@ window.__ModuleLoader__.load({
4372
5318
  type: "button",
4373
5319
  className: "dab-btn",
4374
5320
  onClick: () => {
5321
+ setPaused(false);
4375
5322
  regenerateBg();
4376
5323
  setSpinTick((x) => x + 1);
4377
5324
  },
@@ -4379,7 +5326,7 @@ window.__ModuleLoader__.load({
4379
5326
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4380
5327
  type: "button",
4381
5328
  className: "dab-btn dab-btn-danger",
4382
- onClick: () => setWp(null),
5329
+ onClick: () => setWpFromServer(null),
4383
5330
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 }), t("bgRemove")]
4384
5331
  })]
4385
5332
  })
@@ -4425,7 +5372,7 @@ window.__ModuleLoader__.load({
4425
5372
  ]
4426
5373
  })
4427
5374
  }),
4428
- !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5375
+ !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4429
5376
  className: "dab-card dab-card-hover dab-rise",
4430
5377
  style: { "--d": 3 },
4431
5378
  children: [
@@ -4440,7 +5387,7 @@ window.__ModuleLoader__.load({
4440
5387
  }),
4441
5388
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4442
5389
  type: "button",
4443
- className: "dab-btn dab-btn-ghost",
5390
+ className: "dab-btn dab-btn-soft",
4444
5391
  onClick: () => setUrlOpen((o) => !o),
4445
5392
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkIcon, { size: 14 }), t("bgFromUrl")]
4446
5393
  }),
@@ -4453,21 +5400,14 @@ window.__ModuleLoader__.load({
4453
5400
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 14 }), t("bgEdit")]
4454
5401
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4455
5402
  type: "button",
4456
- className: "dab-btn dab-btn-ghost dab-btn-danger",
4457
- onClick: () => setWp(null),
5403
+ className: "dab-btn dab-btn-danger-inverted",
5404
+ onClick: () => setWpFromServer(null),
4458
5405
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("bgRemove")]
4459
5406
  })] }) : null
4460
5407
  ]
4461
5408
  }),
4462
5409
  urlOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4463
5410
  className: "dab-urlrow",
4464
- style: {
4465
- marginTop: 12,
4466
- display: "flex",
4467
- flexWrap: "wrap",
4468
- gap: 8,
4469
- alignItems: "center"
4470
- },
4471
5411
  children: [
4472
5412
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4473
5413
  type: "text",
@@ -4527,15 +5467,130 @@ window.__ModuleLoader__.load({
4527
5467
  })]
4528
5468
  })
4529
5469
  ]
4530
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5470
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5471
+ className: "dab-card dab-rise",
5472
+ style: { "--d": 4 },
5473
+ children: [
5474
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5475
+ className: "dab-row-head",
5476
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5477
+ className: "dab-swatch-title",
5478
+ style: { marginBottom: 0 },
5479
+ children: t("rotTitle")
5480
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5481
+ type: "button",
5482
+ className: `dab-toggle${rotation.enabled ? " is-on" : ""}`,
5483
+ role: "switch",
5484
+ "aria-checked": rotation.enabled,
5485
+ onClick: () => setRotation({ enabled: !rotation.enabled }),
5486
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
5487
+ })]
5488
+ }),
5489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5490
+ className: "dab-hint",
5491
+ style: { marginTop: 8 },
5492
+ children: t("rotHint")
5493
+ }),
5494
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5495
+ className: "dab-thumbstrip",
5496
+ children: [rotation.items.map((it, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5497
+ className: `dab-thumb${rotation.enabled && i === rotation.current ? " is-current" : ""}`,
5498
+ children: [it.thumb ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
5499
+ src: it.thumb,
5500
+ alt: "",
5501
+ draggable: false
5502
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhotoIcon, { size: 15 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5503
+ type: "button",
5504
+ className: "dab-thumb-x",
5505
+ title: t("rotRemove"),
5506
+ disabled: rotBusy,
5507
+ onClick: () => {
5508
+ setRotBusy(true);
5509
+ removeRotationItem(i).finally(() => setRotBusy(false));
5510
+ },
5511
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XIcon, { size: 10 })
5512
+ })]
5513
+ }, it.file)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5514
+ type: "button",
5515
+ className: "dab-thumb dab-thumb-add",
5516
+ title: t("rotAdd"),
5517
+ disabled: rotBusy,
5518
+ onClick: () => rotFileRef.current?.click(),
5519
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, { size: 16 })
5520
+ })]
5521
+ }),
5522
+ rotation.items.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5523
+ className: "dab-chip-row",
5524
+ style: { marginTop: 12 },
5525
+ children: [
5526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5527
+ type: "button",
5528
+ className: `dab-chip${rotation.mode === "shuffle" ? " is-active" : ""}`,
5529
+ onClick: () => setRotation({ mode: "shuffle" }),
5530
+ children: t("rotShuffle")
5531
+ }),
5532
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5533
+ type: "button",
5534
+ className: `dab-chip${rotation.mode === "order" ? " is-active" : ""}`,
5535
+ onClick: () => setRotation({ mode: "order" }),
5536
+ children: t("rotOrder")
5537
+ }),
5538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-chip-sep" }),
5539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5540
+ type: "button",
5541
+ className: `dab-chip${rotation.interval === "reload" ? " is-active" : ""}`,
5542
+ onClick: () => setRotation({ interval: "reload" }),
5543
+ children: t("rotReload")
5544
+ }),
5545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5546
+ type: "button",
5547
+ className: `dab-chip${rotation.interval === "daily" ? " is-active" : ""}`,
5548
+ onClick: () => setRotation({ interval: "daily" }),
5549
+ children: t("rotDaily")
5550
+ }),
5551
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5552
+ type: "button",
5553
+ className: `dab-chip${rotation.interval === "weekly" ? " is-active" : ""}`,
5554
+ onClick: () => setRotation({ interval: "weekly" }),
5555
+ children: t("rotWeekly")
5556
+ }),
5557
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5558
+ type: "button",
5559
+ className: "dab-btn",
5560
+ disabled: rotBusy,
5561
+ onClick: () => {
5562
+ setRotBusy(true);
5563
+ rotateNow().finally(() => setRotBusy(false));
5564
+ },
5565
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 13 }), t("rotNow")]
5566
+ })
5567
+ ]
5568
+ }) : null,
5569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5570
+ ref: rotFileRef,
5571
+ type: "file",
5572
+ accept: "image/*",
5573
+ multiple: true,
5574
+ style: { display: "none" },
5575
+ onChange: (e) => {
5576
+ const files = Array.from(e.target.files ?? []);
5577
+ e.target.value = "";
5578
+ if (files.length === 0) return;
5579
+ setRotBusy(true);
5580
+ addRotationItems(files).finally(() => setRotBusy(false));
5581
+ }
5582
+ })
5583
+ ]
5584
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4531
5585
  className: "dab-card dab-rise",
4532
5586
  style: { "--d": 3 },
4533
5587
  children: [
4534
5588
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4535
5589
  className: "dab-types",
4536
- children: typeMeta.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5590
+ children: typeMeta.map((m, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4537
5591
  type: "button",
4538
5592
  className: `dab-type${activeGenType === m.type ? " is-active" : ""}`,
5593
+ style: { "--i": i },
4539
5594
  onClick: () => setGenType(m.type),
4540
5595
  children: [
4541
5596
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `dab-type-thumb ${m.thumb}` }),
@@ -4564,7 +5619,8 @@ window.__ModuleLoader__.load({
4564
5619
  children: [
4565
5620
  "aurora",
4566
5621
  "nebula",
4567
- "noise"
5622
+ "noise",
5623
+ "starfield"
4568
5624
  ].map((pr) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4569
5625
  type: "button",
4570
5626
  className: `dab-chip${generatedBg.preset === pr ? " is-active" : ""}`,
@@ -4583,7 +5639,10 @@ window.__ModuleLoader__.load({
4583
5639
  children: [
4584
5640
  "dots",
4585
5641
  "waves",
4586
- "poly"
5642
+ "poly",
5643
+ "rain",
5644
+ "contour",
5645
+ "meta"
4587
5646
  ].map((pr) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4588
5647
  type: "button",
4589
5648
  className: `dab-chip${generatedBg.preset === pr ? " is-active" : ""}`,
@@ -4673,11 +5732,26 @@ window.__ModuleLoader__.load({
4673
5732
  onClick: () => setRegenerateOnReload(!regenerateOnReload),
4674
5733
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
4675
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
+ }),
4676
5749
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4677
5750
  type: "button",
4678
5751
  className: "dab-btn dab-btn-primary",
4679
5752
  onClick: () => {
4680
5753
  regenerateBg();
5754
+ setPaused(false);
4681
5755
  setSpinTick((x) => x + 1);
4682
5756
  },
4683
5757
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -4692,7 +5766,7 @@ window.__ModuleLoader__.load({
4692
5766
  }),
4693
5767
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4694
5768
  className: "dab-card dab-card-hover dab-rise",
4695
- style: { "--d": 4 },
5769
+ style: { "--d": 5 },
4696
5770
  children: [
4697
5771
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
4698
5772
  label: t("wpOpacity"),
@@ -4764,10 +5838,187 @@ window.__ModuleLoader__.load({
4764
5838
  ] });
4765
5839
  }
4766
5840
  //#endregion
5841
+ //#region src/client/utils/presets.ts
5842
+ const zeroBlurs = {
5843
+ bg: 0,
5844
+ sidebar: 0,
5845
+ card: 0,
5846
+ settings: 0,
5847
+ chat: 0,
5848
+ trajectory: 0,
5849
+ input: 0
5850
+ };
5851
+ const BUILTIN_PRESETS = [
5852
+ {
5853
+ key: "default",
5854
+ appearance: {
5855
+ color: null,
5856
+ opacities: {
5857
+ bg: .85,
5858
+ sidebar: .93,
5859
+ card: 1,
5860
+ input: 1
5861
+ },
5862
+ blurs: { ...zeroBlurs },
5863
+ settingsOpacity: 1,
5864
+ wallpaperOpacity: 1,
5865
+ blur: 0,
5866
+ chatTextOpacity: 0,
5867
+ trajectoryOpacity: 1
5868
+ }
5869
+ },
5870
+ {
5871
+ key: "glass",
5872
+ appearance: {
5873
+ color: [
5874
+ 212,
5875
+ .5,
5876
+ .38
5877
+ ],
5878
+ opacities: {
5879
+ bg: .62,
5880
+ sidebar: .55,
5881
+ card: .62,
5882
+ input: .58
5883
+ },
5884
+ blurs: {
5885
+ ...zeroBlurs,
5886
+ bg: 20,
5887
+ sidebar: 14,
5888
+ card: 12,
5889
+ settings: 20,
5890
+ trajectory: 8,
5891
+ input: 14
5892
+ },
5893
+ settingsOpacity: .88,
5894
+ wallpaperOpacity: 1,
5895
+ blur: 0,
5896
+ chatTextOpacity: 0,
5897
+ trajectoryOpacity: .85
5898
+ }
5899
+ },
5900
+ {
5901
+ key: "minimal",
5902
+ appearance: {
5903
+ color: null,
5904
+ opacities: {
5905
+ bg: .97,
5906
+ sidebar: .97,
5907
+ card: 1,
5908
+ input: 1
5909
+ },
5910
+ blurs: { ...zeroBlurs },
5911
+ settingsOpacity: 1,
5912
+ wallpaperOpacity: 1,
5913
+ blur: 0,
5914
+ chatTextOpacity: 0,
5915
+ trajectoryOpacity: 1
5916
+ }
5917
+ },
5918
+ {
5919
+ key: "midnight",
5920
+ appearance: {
5921
+ color: [
5922
+ 262,
5923
+ .45,
5924
+ .16
5925
+ ],
5926
+ opacities: {
5927
+ bg: .5,
5928
+ sidebar: .45,
5929
+ card: .55,
5930
+ input: .6
5931
+ },
5932
+ blurs: {
5933
+ ...zeroBlurs,
5934
+ bg: 24,
5935
+ sidebar: 18,
5936
+ card: 14,
5937
+ settings: 22,
5938
+ trajectory: 10,
5939
+ input: 16
5940
+ },
5941
+ settingsOpacity: .85,
5942
+ wallpaperOpacity: .92,
5943
+ blur: 2,
5944
+ chatTextOpacity: 0,
5945
+ trajectoryOpacity: .8
5946
+ }
5947
+ },
5948
+ {
5949
+ key: "cyber",
5950
+ appearance: {
5951
+ color: [
5952
+ 315,
5953
+ .7,
5954
+ .22
5955
+ ],
5956
+ opacities: {
5957
+ bg: .42,
5958
+ sidebar: .5,
5959
+ card: .45,
5960
+ input: .55
5961
+ },
5962
+ blurs: {
5963
+ ...zeroBlurs,
5964
+ bg: 16,
5965
+ sidebar: 12,
5966
+ card: 10,
5967
+ settings: 18,
5968
+ trajectory: 6,
5969
+ input: 20
5970
+ },
5971
+ settingsOpacity: .8,
5972
+ wallpaperOpacity: 1,
5973
+ blur: 0,
5974
+ chatTextOpacity: 0,
5975
+ trajectoryOpacity: .78
5976
+ }
5977
+ },
5978
+ {
5979
+ key: "warm",
5980
+ appearance: {
5981
+ color: [
5982
+ 28,
5983
+ .6,
5984
+ .72
5985
+ ],
5986
+ opacities: {
5987
+ bg: .92,
5988
+ sidebar: .95,
5989
+ card: 1,
5990
+ input: 1
5991
+ },
5992
+ blurs: { ...zeroBlurs },
5993
+ settingsOpacity: 1,
5994
+ wallpaperOpacity: 1,
5995
+ blur: 0,
5996
+ chatTextOpacity: 0,
5997
+ trajectoryOpacity: 1
5998
+ }
5999
+ }
6000
+ ];
6001
+ //#endregion
4767
6002
  //#region src/client/components/pages/ProfilePage.tsx
6003
+ const hexOf = (color) => color === null ? "" : "#" + hslToRgb(color[0], color[1], color[2]).map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
6004
+ function ProfileDot({ entry }) {
6005
+ const hex = hexOf(entry.config.color);
6006
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6007
+ className: "dab-profile-dot",
6008
+ style: hex ? { background: hex } : void 0,
6009
+ title: hex ? hex.toUpperCase() : void 0,
6010
+ children: hex ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 11 })
6011
+ });
6012
+ }
4768
6013
  function ProfilePage({ p, notify }) {
4769
- const { t, exportTheme, importTheme } = p;
6014
+ const { t, exportTheme, importTheme, saveProfile, applyProfile, deleteProfile, applyPreset, setSchedule, useStore } = p;
6015
+ const profiles = useStore((s) => s.profiles);
6016
+ const schedule = useStore((s) => s.schedule);
6017
+ const activeProfile = useStore((s) => s.activeProfile);
4770
6018
  const importRef = (0, react.useRef)(null);
6019
+ const [nameOpen, setNameOpen] = (0, react.useState)(false);
6020
+ const [nameVal, setNameVal] = (0, react.useState)("");
6021
+ const [confirmId, setConfirmId] = (0, react.useState)(null);
4771
6022
  const onImport = async (file) => {
4772
6023
  try {
4773
6024
  const ok = await importTheme(file);
@@ -4776,6 +6027,39 @@ window.__ModuleLoader__.load({
4776
6027
  notify(t("importFail"), false);
4777
6028
  }
4778
6029
  };
6030
+ const onSave = () => {
6031
+ const name = nameVal.trim();
6032
+ if (!name) return;
6033
+ if (saveProfile(name)) {
6034
+ notify(t("profileSaved"), true);
6035
+ setNameVal("");
6036
+ setNameOpen(false);
6037
+ } else notify(t("profileNameRequired"), false);
6038
+ };
6039
+ const onApply = (entry) => {
6040
+ if (applyProfile(entry.id)) notify(t("profileApplied"), true);
6041
+ };
6042
+ const onDelete = (entry) => {
6043
+ if (confirmId !== entry.id) {
6044
+ setConfirmId(entry.id);
6045
+ return;
6046
+ }
6047
+ setConfirmId(null);
6048
+ deleteProfile(entry.id);
6049
+ };
6050
+ const profileOptions = (value, onChange) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
6051
+ className: "dab-select",
6052
+ value: value ?? "",
6053
+ onChange: (e) => onChange(e.target.value || null),
6054
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6055
+ value: "",
6056
+ children: t("profileNone")
6057
+ }), profiles.map((pr) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
6058
+ value: pr.id,
6059
+ children: pr.name
6060
+ }, pr.id))]
6061
+ });
6062
+ const patchSchedule = (patch) => setSchedule(patch);
4779
6063
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4780
6064
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
4781
6065
  className: "dab-head dab-rise",
@@ -4795,11 +6079,263 @@ window.__ModuleLoader__.load({
4795
6079
  })
4796
6080
  ]
4797
6081
  }),
6082
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
6083
+ className: "dab-card dab-rise",
6084
+ style: { "--d": 1 },
6085
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6086
+ className: "dab-swatch-title",
6087
+ children: t("presetGalleryTitle")
6088
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6089
+ className: "dab-preset-grid",
6090
+ children: BUILTIN_PRESETS.map((ps, i) => {
6091
+ const hex = hexOf(ps.appearance.color);
6092
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6093
+ type: "button",
6094
+ className: "dab-preset",
6095
+ style: { "--i": i },
6096
+ onClick: () => {
6097
+ applyPreset(ps.appearance);
6098
+ notify(t("presetApplied"));
6099
+ },
6100
+ children: [
6101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6102
+ className: "dab-preset-dot",
6103
+ style: hex ? { background: hex } : void 0,
6104
+ children: hex ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 12 })
6105
+ }),
6106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6107
+ className: "dab-preset-name",
6108
+ children: t(`presetName_${ps.key}`)
6109
+ }),
6110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6111
+ className: "dab-preset-desc",
6112
+ children: t(`presetDesc_${ps.key}`)
6113
+ })
6114
+ ]
6115
+ }, ps.key);
6116
+ })
6117
+ })]
6118
+ }),
6119
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
6120
+ className: "dab-card dab-rise",
6121
+ style: { "--d": 2 },
6122
+ children: [
6123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6124
+ className: "dab-row-head",
6125
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6126
+ className: "dab-swatch-title",
6127
+ style: { marginBottom: 0 },
6128
+ children: t("profilesTitle")
6129
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6130
+ type: "button",
6131
+ className: "dab-btn dab-btn-primary",
6132
+ onClick: () => setNameOpen((o) => !o),
6133
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, { size: 14 }), t("profileSave")]
6134
+ })]
6135
+ }),
6136
+ nameOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6137
+ className: "dab-urlrow",
6138
+ children: [
6139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6140
+ type: "text",
6141
+ className: "dab-urlinput",
6142
+ value: nameVal,
6143
+ placeholder: t("profileNamePlaceholder"),
6144
+ maxLength: 60,
6145
+ onChange: (e) => setNameVal(e.target.value),
6146
+ onKeyDown: (e) => {
6147
+ if (e.key === "Enter") {
6148
+ e.preventDefault();
6149
+ onSave();
6150
+ }
6151
+ },
6152
+ autoFocus: true
6153
+ }),
6154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6155
+ type: "button",
6156
+ className: "dab-btn dab-btn-primary",
6157
+ disabled: !nameVal.trim(),
6158
+ onClick: onSave,
6159
+ children: t("editorCommit")
6160
+ }),
6161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6162
+ type: "button",
6163
+ className: "dab-btn",
6164
+ onClick: () => {
6165
+ setNameOpen(false);
6166
+ setNameVal("");
6167
+ },
6168
+ children: t("bgUrlCancel")
6169
+ })
6170
+ ]
6171
+ }) : null,
6172
+ profiles.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6173
+ className: "dab-hint",
6174
+ style: { marginTop: 10 },
6175
+ children: t("profilesEmpty")
6176
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6177
+ className: "dab-profile-list",
6178
+ children: profiles.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6179
+ className: `dab-profile-row${activeProfile === entry.id ? " is-active" : ""}`,
6180
+ style: { "--i": i },
6181
+ children: [
6182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfileDot, { entry }),
6183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6184
+ className: "dab-profile-meta",
6185
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6186
+ className: "dab-profile-name",
6187
+ children: entry.name
6188
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6189
+ className: "dab-profile-sub",
6190
+ children: new Date(entry.createdAt).toLocaleDateString()
6191
+ })]
6192
+ }),
6193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6194
+ type: "button",
6195
+ className: "dab-btn",
6196
+ onClick: () => onApply(entry),
6197
+ children: t("profileApply")
6198
+ }),
6199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6200
+ type: "button",
6201
+ className: `dab-btn dab-btn-danger-solid${confirmId === entry.id ? " dab-btn-confirm" : ""}`,
6202
+ title: t("profileDelete"),
6203
+ onClick: () => onDelete(entry),
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")
6205
+ })
6206
+ ]
6207
+ }, entry.id))
6208
+ })
6209
+ ]
6210
+ }),
6211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
6212
+ className: "dab-card dab-rise",
6213
+ style: { "--d": 3 },
6214
+ children: [
6215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6216
+ className: "dab-row-head",
6217
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6218
+ className: "dab-swatch-title",
6219
+ style: { marginBottom: 0 },
6220
+ children: t("scheduleTitle")
6221
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6222
+ type: "button",
6223
+ className: `dab-toggle${schedule.enabled ? " is-on" : ""}`,
6224
+ role: "switch",
6225
+ "aria-checked": schedule.enabled,
6226
+ onClick: () => patchSchedule({ enabled: !schedule.enabled }),
6227
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
6228
+ })]
6229
+ }),
6230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6231
+ className: "dab-hint",
6232
+ style: { marginTop: 8 },
6233
+ children: t("scheduleHint")
6234
+ }),
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
+ ]
6329
+ })
6330
+ })
6331
+ })
6332
+ ]
6333
+ }),
4798
6334
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4799
6335
  className: "dab-profile-grid",
4800
6336
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4801
6337
  className: "dab-card dab-card-hover dab-rise",
4802
- style: { "--d": 1 },
6338
+ style: { "--d": 4 },
4803
6339
  children: [
4804
6340
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4805
6341
  className: "dab-profile-ico",
@@ -4825,7 +6361,7 @@ window.__ModuleLoader__.load({
4825
6361
  ]
4826
6362
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4827
6363
  className: "dab-card dab-card-hover dab-rise",
4828
- style: { "--d": 2 },
6364
+ style: { "--d": 5 },
4829
6365
  children: [
4830
6366
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4831
6367
  className: "dab-profile-ico",
@@ -4862,7 +6398,7 @@ window.__ModuleLoader__.load({
4862
6398
  }),
4863
6399
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
4864
6400
  className: "dab-footer dab-rise",
4865
- style: { "--d": 3 },
6401
+ style: { "--d": 6 },
4866
6402
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4867
6403
  className: "dab-footer-mono",
4868
6404
  children: "dsh-any-background"
@@ -4996,7 +6532,30 @@ window.__ModuleLoader__.load({
4996
6532
  const registerCustom = (h, s, l) => {
4997
6533
  customDispose?.();
4998
6534
  try {
4999
- const { colorScheme, tokens } = genTokens(h, s, l);
6535
+ let colorScheme;
6536
+ let tokens;
6537
+ if (rHasColor()) ({colorScheme, tokens} = genTokens(h ?? rColor()[0], s ?? rColor()[1], l ?? rColor()[2], rColorScheme()));
6538
+ else if (rSchemeOverride() !== "auto") {
6539
+ const dark = rScheme() === "dark";
6540
+ ({colorScheme, tokens} = genTokens(220, .04, dark ? .14 : .92, dark ? "dark" : "light"));
6541
+ } else {
6542
+ const verdict = rBgDark();
6543
+ if (verdict === null) {
6544
+ customDispose = null;
6545
+ return false;
6546
+ }
6547
+ const snap = ctx.theme.getTheme();
6548
+ const wantScheme = verdict ? "dark" : "light";
6549
+ const source = snap.themes.find((t) => t.id !== CUSTOM_ID && t.colorScheme === wantScheme) ?? snap.themes.find((t) => t.id !== CUSTOM_ID);
6550
+ if (source === void 0) {
6551
+ customDispose = null;
6552
+ return false;
6553
+ }
6554
+ colorScheme = wantScheme;
6555
+ tokens = { ...source.tokens };
6556
+ const font = verdict ? "#fff" : "#000";
6557
+ for (const name of LABEL_TOKENS) tokens[name] = font;
6558
+ }
5000
6559
  customDispose = ctx.theme.register({
5001
6560
  id: CUSTOM_ID,
5002
6561
  colorScheme,
@@ -5005,16 +6564,27 @@ window.__ModuleLoader__.load({
5005
6564
  } catch {
5006
6565
  customDispose = null;
5007
6566
  }
5008
- if (ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
6567
+ const present = ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
6568
+ if (present) ctx.theme.setTheme(CUSTOM_ID);
6569
+ return present;
5009
6570
  };
5010
6571
  if (rHasColor()) registerCustom(initH, initS, initL);
6572
+ onVerdictApplied(() => {
6573
+ if (!rHasColor()) registerCustom();
6574
+ });
6575
+ onColorAdopted((hsl) => {
6576
+ registerCustom(hsl[0], hsl[1], hsl[2]);
6577
+ saveConfig();
6578
+ colorRev++;
6579
+ bound?.syncColor(hslToHsv(hsl[0], hsl[1], hsl[2]), colorRev);
6580
+ });
5011
6581
  ctx.effect(() => () => {
5012
6582
  customDispose?.();
5013
6583
  if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
5014
6584
  }, "dsh-any-background: skin dispose");
5015
6585
  const styleEl = document.createElement("style");
5016
6586
  styleEl.dataset.plugin = "dsh-any-background";
5017
- 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}`;
5018
6588
  document.head.appendChild(styleEl);
5019
6589
  ctx.effect(() => () => {
5020
6590
  styleEl?.parentNode?.removeChild(styleEl);
@@ -5034,7 +6604,16 @@ window.__ModuleLoader__.load({
5034
6604
  backgroundType: cfg.backgroundType,
5035
6605
  generatedBg: cfg.generatedBg,
5036
6606
  bgRev: -1,
5037
- regenerateOnReload: cfg.regenerateOnReload
6607
+ regenerateOnReload: cfg.regenerateOnReload,
6608
+ profiles: [],
6609
+ rotation: {
6610
+ ...DEFAULT_CONFIG.rotation,
6611
+ items: []
6612
+ },
6613
+ schedule: { ...DEFAULT_CONFIG.schedule },
6614
+ schemeOverride: "auto",
6615
+ activeProfile: null,
6616
+ metaRev: -1
5038
6617
  }),
5039
6618
  actions: {
5040
6619
  syncBg: (d, url, r, bgType, genBg, bgr, reload) => {
@@ -5054,6 +6633,16 @@ window.__ModuleLoader__.load({
5054
6633
  d.color = hsv;
5055
6634
  d.colorRev = r;
5056
6635
  }
6636
+ },
6637
+ syncMeta: (d, profiles, rotation, schedule, schemeOverride, activeProfile, r) => {
6638
+ if (r > d.metaRev) {
6639
+ d.profiles = profiles;
6640
+ d.rotation = rotation;
6641
+ d.schedule = schedule;
6642
+ d.schemeOverride = schemeOverride;
6643
+ d.activeProfile = activeProfile;
6644
+ d.metaRev = r;
6645
+ }
5057
6646
  }
5058
6647
  }
5059
6648
  });
@@ -5064,14 +6653,145 @@ window.__ModuleLoader__.load({
5064
6653
  bound?.syncBg(rWp(), rev, cfg.backgroundType, cfg.generatedBg, bgRev, cfg.regenerateOnReload);
5065
6654
  };
5066
6655
  onGeneratedSnapshot(syncBg);
6656
+ let metaRev = 0;
6657
+ const syncMetaNow = () => {
6658
+ metaRev++;
6659
+ bound?.syncMeta(rProfiles(), rRotation(), rSchedule(), rSchemeOverride(), cfg.activeProfile, metaRev);
6660
+ };
6661
+ /** Apply an appearance snapshot (profile or built-in preset) to the whole
6662
+ * interface: re-register the skin, re-emit tokens, persist. */
6663
+ const applyAppearanceLive = (ap) => {
6664
+ applyAppearance(ap);
6665
+ if (rHasColor()) {
6666
+ const [h, s, l] = rColor();
6667
+ registerCustom(h, s, l);
6668
+ } else if (!registerCustom()) {
6669
+ customDispose?.();
6670
+ customDispose = null;
6671
+ clearThemeTokens();
6672
+ const snap = ctx.theme.getTheme();
6673
+ const fallback = snap.themes.find((t) => t.id !== CUSTOM_ID);
6674
+ if (fallback !== void 0 && snap.preference === CUSTOM_ID) ctx.theme.setTheme(fallback.id);
6675
+ }
6676
+ applyWp();
6677
+ };
6678
+ const applyProfileById = (id) => {
6679
+ const entry = rProfiles().find((p) => p.id === id);
6680
+ if (entry === void 0) return false;
6681
+ applyAppearanceLive(entry.config);
6682
+ cfg.activeProfile = id;
6683
+ applyWp();
6684
+ persistConfig();
6685
+ syncMetaNow();
6686
+ return true;
6687
+ };
6688
+ /** Extract the theme color once from a freshly activated wallpaper and run
6689
+ * the full adaptation (host skin + editor wheel sync); the caller persists. */
6690
+ const adoptWallpaperColor = async (dataUrl) => {
6691
+ const hsl = await extractWallpaperColor(dataUrl, rBgState());
6692
+ if (!hsl || rWp() !== dataUrl) return;
6693
+ cfg.color = hsl;
6694
+ registerCustom(hsl[0], hsl[1], hsl[2]);
6695
+ colorRev++;
6696
+ bound?.syncColor(hslToHsv(hsl[0], hsl[1], hsl[2]), colorRev);
6697
+ };
6698
+ /** Activate a rotation item: the server copies its bytes into the wallpaper
6699
+ * slot; the client applies the returned serve URL through the normal image
6700
+ * path, then re-extracts the theme color from the new picture so the
6701
+ * palette follows the rotation. */
6702
+ const applyRotationIndex = async (idx, auto) => {
6703
+ const rot = rRotation();
6704
+ if (idx < 0 || idx >= rot.items.length) return false;
6705
+ const r = await rotationActivate(idx);
6706
+ if (!r.ok || !r.wallpaperUrl) {
6707
+ console.warn("dsh-any-background: rotation activate failed", r.error);
6708
+ return false;
6709
+ }
6710
+ cfg.backgroundType = "image";
6711
+ setBgDark(null);
6712
+ setWpImageUrl(r.wallpaperUrl);
6713
+ setWpUrl(rWpImage());
6714
+ setBgState({ ...DEFAULT_CONFIG.bgState });
6715
+ cfg.rotation = {
6716
+ ...rot,
6717
+ current: idx,
6718
+ lastRotate: auto || rot.lastRotate === null ? (/* @__PURE__ */ new Date()).toISOString() : rot.lastRotate
6719
+ };
6720
+ if (cfg.backgroundType === "image") await adoptWallpaperColor(rWp());
6721
+ applyThemeColor();
6722
+ syncBg();
6723
+ saveConfig();
6724
+ syncMetaNow();
6725
+ return true;
6726
+ };
6727
+ const isoWeekKey = (d) => {
6728
+ const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
6729
+ const day = t.getUTCDay() || 7;
6730
+ t.setUTCDate(t.getUTCDate() + 4 - day);
6731
+ const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
6732
+ const week = Math.ceil(((t.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
6733
+ return `${t.getUTCFullYear()}-W${week}`;
6734
+ };
6735
+ const rotationDue = () => {
6736
+ const rot = rRotation();
6737
+ if (!rot.enabled || rot.items.length === 0) return false;
6738
+ if (rot.interval === "reload") return true;
6739
+ const last = rot.lastRotate !== null ? new Date(rot.lastRotate) : null;
6740
+ if (last === null || isNaN(last.getTime())) return true;
6741
+ const now = /* @__PURE__ */ new Date();
6742
+ if (rot.interval === "daily") return last.toDateString() !== now.toDateString();
6743
+ return isoWeekKey(last) !== isoWeekKey(now);
6744
+ };
6745
+ const pickNextRotationIndex = () => {
6746
+ const rot = rRotation();
6747
+ const n = rot.items.length;
6748
+ if (n === 0) return -1;
6749
+ if (rot.mode === "shuffle" && n > 1) {
6750
+ let idx = rot.current;
6751
+ while (idx === rot.current) idx = Math.floor(Math.random() * n);
6752
+ return idx;
6753
+ }
6754
+ return (rot.current + 1) % n;
6755
+ };
6756
+ const maybeRotate = async () => {
6757
+ if (!rotationDue()) return;
6758
+ await applyRotationIndex(pickNextRotationIndex(), true);
6759
+ };
6760
+ const parseHHMM = (s) => {
6761
+ const m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(s);
6762
+ return m === null ? -1 : Number(m[1]) * 60 + Number(m[2]);
6763
+ };
6764
+ const isNightNow = (sc) => {
6765
+ const now = /* @__PURE__ */ new Date();
6766
+ const cur = now.getHours() * 60 + now.getMinutes();
6767
+ const day = parseHHMM(sc.dayStart);
6768
+ const night = parseHHMM(sc.nightStart);
6769
+ if (day < 0 || night < 0) return false;
6770
+ if (day <= night) return cur >= night || cur < day;
6771
+ return cur >= night && cur < day;
6772
+ };
6773
+ const scheduleTick = () => {
6774
+ const sc = rSchedule();
6775
+ if (!sc.enabled) return;
6776
+ const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false : isNightNow(sc)) ? sc.nightProfile : sc.dayProfile;
6777
+ if (!want || want === cfg.activeProfile) return;
6778
+ applyProfileById(want);
6779
+ };
5067
6780
  applyWp();
5068
6781
  syncBg();
5069
6782
  watchParts();
5070
- loadPersisted().then(() => {
6783
+ loadPersisted().then(async (serverRotated) => {
6784
+ syncMetaNow();
5071
6785
  if (rHasColor()) {
5072
6786
  const [h, s, l] = rColor();
5073
6787
  registerCustom(h, s, l);
5074
6788
  }
6789
+ if (serverRotated) {
6790
+ if (cfg.backgroundType === "image" && rWp()) {
6791
+ await adoptWallpaperColor(rWp());
6792
+ saveConfig();
6793
+ }
6794
+ } else await maybeRotate();
5075
6795
  if (cfg.backgroundType === "video") {
5076
6796
  const v = rWpVideo();
5077
6797
  if (v) {
@@ -5093,16 +6813,24 @@ window.__ModuleLoader__.load({
5093
6813
  persistConfig();
5094
6814
  } else applyThemeColor();
5095
6815
  syncBg();
6816
+ scheduleTick();
5096
6817
  if (rHasColor()) {
5097
6818
  colorRev++;
5098
6819
  bound?.syncColor(hslToHsv(...rColor()), colorRev);
5099
6820
  }
5100
6821
  });
6822
+ const schemeMq = window.matchMedia?.("(prefers-color-scheme: dark)");
6823
+ const scheduleTimer = window.setInterval(scheduleTick, 3e4);
6824
+ schemeMq?.addEventListener?.("change", scheduleTick);
6825
+ ctx.effect(() => () => {
6826
+ window.clearInterval(scheduleTimer);
6827
+ schemeMq?.removeEventListener?.("change", scheduleTick);
6828
+ }, "dsh-any-background: schedule timer");
5101
6829
  ctx.effect(() => () => {
5102
6830
  teardownWp();
5103
6831
  }, "dsh-any-background: wp cleanup");
5104
6832
  ctx.effect(() => ctx.on("theme/change", () => {
5105
- if (rHasColor()) {
6833
+ if (rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto") {
5106
6834
  const snapshot = ctx.theme.getTheme();
5107
6835
  if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
5108
6836
  }
@@ -5135,6 +6863,7 @@ window.__ModuleLoader__.load({
5135
6863
  const sectionInject = (actions) => {
5136
6864
  bound = actions;
5137
6865
  syncBg();
6866
+ syncMetaNow();
5138
6867
  const playVideoFromBlob = (blob, mime) => {
5139
6868
  const localUrl = URL.createObjectURL(blob);
5140
6869
  cfg.backgroundType = "video";
@@ -5182,16 +6911,16 @@ window.__ModuleLoader__.load({
5182
6911
  nl
5183
6912
  ], colorRev);
5184
6913
  },
5185
- setWp: (u) => {
6914
+ setWpFromServer: (u) => {
5186
6915
  cfg.backgroundType = "image";
5187
6916
  setBgDark(null);
5188
6917
  setWpImageUrl(u);
5189
- setWpUrl(u);
6918
+ setWpUrl(rWpImage());
5190
6919
  setBgState({ ...DEFAULT_CONFIG.bgState });
5191
- persistWallpaper(u);
5192
6920
  if (u === null) {
5193
6921
  setWpVideoUrl(null, null);
5194
6922
  persistVideo(null);
6923
+ persistWallpaper(null);
5195
6924
  }
5196
6925
  applyThemeColor();
5197
6926
  syncBg();
@@ -5296,23 +7025,29 @@ window.__ModuleLoader__.load({
5296
7025
  if (cfg.backgroundType === "video") {
5297
7026
  const vurl = rWpVideo();
5298
7027
  if (vurl) try {
5299
- const blob = await fetch(vurl).then((r) => r.blob());
5300
- videoPayload = await new Promise((resolve, reject) => {
5301
- const fr = new FileReader();
5302
- fr.onload = () => resolve(fr.result);
5303
- fr.onerror = () => reject(fr.error);
5304
- fr.readAsDataURL(blob);
5305
- });
7028
+ videoPayload = await blobToDataUrl(await fetch(vurl).then((r) => r.blob()));
5306
7029
  if (videoPayload && !/^data:video\//.test(videoPayload)) videoPayload = videoPayload.replace(/^data:[^;,]*/, `data:${cfg.videoMime ?? "video/mp4"}`);
5307
7030
  } catch {
5308
7031
  videoPayload = null;
5309
7032
  }
5310
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
+ }
5311
7046
  const payload = {
5312
7047
  version: 2,
5313
7048
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
5314
7049
  config: cfg,
5315
- wallpaper: cfg.backgroundType === "image" ? rWp() : null,
7050
+ wallpaper: wallpaperPayload,
5316
7051
  video: videoPayload
5317
7052
  };
5318
7053
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
@@ -5388,6 +7123,134 @@ window.__ModuleLoader__.load({
5388
7123
  } catch {
5389
7124
  return false;
5390
7125
  }
7126
+ },
7127
+ saveProfile: (name) => {
7128
+ const trimmed = name.trim();
7129
+ if (!trimmed) return false;
7130
+ const profiles = [...rProfiles()];
7131
+ if (profiles.length >= 20) profiles.shift();
7132
+ const id = `p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
7133
+ profiles.push({
7134
+ id,
7135
+ name: trimmed.slice(0, 60),
7136
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7137
+ config: currentAppearance()
7138
+ });
7139
+ cfg.profiles = profiles;
7140
+ cfg.activeProfile = id;
7141
+ persistConfig();
7142
+ syncMetaNow();
7143
+ return true;
7144
+ },
7145
+ applyProfile: (id) => applyProfileById(id),
7146
+ deleteProfile: (id) => {
7147
+ const before = rProfiles();
7148
+ if (!before.some((p) => p.id === id)) return false;
7149
+ cfg.profiles = before.filter((p) => p.id !== id);
7150
+ if (cfg.activeProfile === id) cfg.activeProfile = null;
7151
+ const sc = rSchedule();
7152
+ if (sc.dayProfile === id || sc.nightProfile === id) cfg.schedule = {
7153
+ ...sc,
7154
+ dayProfile: sc.dayProfile === id ? null : sc.dayProfile,
7155
+ nightProfile: sc.nightProfile === id ? null : sc.nightProfile
7156
+ };
7157
+ persistConfig();
7158
+ syncMetaNow();
7159
+ return true;
7160
+ },
7161
+ applyPreset: (appearance) => {
7162
+ applyAppearanceLive(appearance);
7163
+ cfg.activeProfile = null;
7164
+ applyWp();
7165
+ persistConfig();
7166
+ syncMetaNow();
7167
+ },
7168
+ setSchemeOverride: (v) => {
7169
+ cfg.schemeOverride = v;
7170
+ registerCustom();
7171
+ applyWp();
7172
+ saveConfig();
7173
+ syncMetaNow();
7174
+ },
7175
+ setSchedule: (patch) => {
7176
+ cfg.schedule = {
7177
+ ...rSchedule(),
7178
+ ...patch
7179
+ };
7180
+ if (patch.enabled === true) scheduleTick();
7181
+ persistConfig();
7182
+ syncMetaNow();
7183
+ },
7184
+ setRotation: (patch) => {
7185
+ cfg.rotation = {
7186
+ ...rRotation(),
7187
+ ...patch
7188
+ };
7189
+ if (patch.enabled === true) maybeRotate();
7190
+ persistConfig();
7191
+ syncMetaNow();
7192
+ },
7193
+ addRotationItems: async (files) => {
7194
+ let added = false;
7195
+ for (const f of files) {
7196
+ if (!f.type.startsWith("image/")) continue;
7197
+ const dataUrl = await readImgAsync(f);
7198
+ if (!dataUrl) continue;
7199
+ const r = await rotationAdd(dataUrl, await makeThumb(dataUrl) ?? "");
7200
+ if (r.ok && r.items !== void 0) {
7201
+ cfg.rotation = {
7202
+ ...rRotation(),
7203
+ items: r.items
7204
+ };
7205
+ added = true;
7206
+ }
7207
+ }
7208
+ if (added) {
7209
+ persistConfig();
7210
+ syncMetaNow();
7211
+ syncBg();
7212
+ }
7213
+ return added;
7214
+ },
7215
+ removeRotationItem: async (index) => {
7216
+ const r = await rotationRemove(index);
7217
+ if (!r.ok || r.items === void 0) return false;
7218
+ const rot = rRotation();
7219
+ cfg.rotation = {
7220
+ ...rot,
7221
+ items: r.items,
7222
+ current: Math.max(0, Math.min(rot.current >= index ? rot.current - 1 : rot.current, Math.max(0, r.items.length - 1)))
7223
+ };
7224
+ persistConfig();
7225
+ syncMetaNow();
7226
+ return true;
7227
+ },
7228
+ rotateNow: async () => {
7229
+ if (rRotation().items.length === 0) return false;
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;
5391
7254
  }
5392
7255
  };
5393
7256
  };
@@ -5463,12 +7326,11 @@ window.__ModuleLoader__.load({
5463
7326
  restoreTimers.forEach((id) => window.clearTimeout(id));
5464
7327
  }, "dsh-any-background: boot restore");
5465
7328
  const watchdogId = window.setInterval(() => {
5466
- if (!rHasColor()) return;
7329
+ if (!rHasColor() && rBgDark() === null && rSchemeOverride() === "auto") return;
5467
7330
  const snapshot = ctx.theme.getTheme();
5468
7331
  let changed = false;
5469
7332
  if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) {
5470
- const [h, s, l] = rColor();
5471
- registerCustom(h, s, l);
7333
+ registerCustom();
5472
7334
  changed = true;
5473
7335
  } else if (snapshot.preference !== CUSTOM_ID) {
5474
7336
  ctx.theme.setTheme(CUSTOM_ID);