dsh-any-background 0.2.4 → 0.2.5

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: "灵感色板",
@@ -121,7 +121,58 @@ window.__ModuleLoader__.load({
121
121
  presetNoise: "流动噪声",
122
122
  presetDots: "点阵",
123
123
  presetWaves: "波浪",
124
- presetPoly: "低多边形"
124
+ presetPoly: "低多边形",
125
+ schemeTitle: "界面明暗",
126
+ schemeLight: "亮色",
127
+ schemeDark: "暗色",
128
+ schemeAuto: "自动",
129
+ schemeHint: "「自动」优先跟随背景画面的实际明暗决定界面明暗(无背景时依据主题色明度);强制亮色/暗色后整套配色按所选方向生成,未选主题色时使用中性灰配色",
130
+ presetGalleryTitle: "外观预设",
131
+ presetName_default: "默认",
132
+ presetDesc_default: "恢复插件的初始外观",
133
+ presetName_glass: "毛玻璃",
134
+ presetDesc_glass: "通透表面 + 磨砂玻璃层次",
135
+ presetName_minimal: "极简白",
136
+ presetDesc_minimal: "接近实色的表面,无模糊",
137
+ presetName_midnight: "暗夜紫",
138
+ presetDesc_midnight: "深色强调,重磨砂低透明度",
139
+ presetName_cyber: "赛博",
140
+ presetDesc_cyber: "艳丽洋红,界面近乎消失",
141
+ presetName_warm: "暖阳",
142
+ presetDesc_warm: "琥珀色调的明亮实色界面",
143
+ presetApplied: "预设已应用",
144
+ profilesTitle: "我的配置",
145
+ profileSave: "保存当前配置",
146
+ profileNamePlaceholder: "输入配置名称…",
147
+ profileNameRequired: "请先输入配置名称",
148
+ profileSaved: "配置已保存",
149
+ profileApplied: "配置已应用",
150
+ profileApply: "应用",
151
+ profileDelete: "删除配置",
152
+ profileDeleteConfirm: "确认删除",
153
+ profileNone: "不切换",
154
+ profilesEmpty: "还没有保存的配置。调整好外观后点击「保存当前配置」,即可随时一键切换。",
155
+ scheduleTitle: "昼夜自动切换",
156
+ scheduleHint: "按时间或系统深色模式,在两套已保存的配置之间自动切换(切换的是配色与表面参数,壁纸不变)",
157
+ scheduleMode: "切换依据",
158
+ scheduleModeTime: "固定时段",
159
+ scheduleModeSystem: "跟随系统",
160
+ scheduleTimes: "切换时间",
161
+ scheduleDayStart: "日间开始",
162
+ scheduleNightStart: "夜间开始",
163
+ scheduleProfiles: "对应配置",
164
+ scheduleDayProfile: "日间",
165
+ scheduleNightProfile: "夜间",
166
+ rotTitle: "壁纸轮换",
167
+ rotHint: "把多张图片加入轮换池,按所选频率自动更换壁纸;启用后立即生效",
168
+ rotAdd: "添加图片",
169
+ rotRemove: "移除",
170
+ rotShuffle: "随机",
171
+ rotOrder: "顺序",
172
+ rotReload: "每次刷新",
173
+ rotDaily: "每天",
174
+ rotWeekly: "每周",
175
+ rotNow: "立即切换"
125
176
  };
126
177
  const en = {
127
178
  nav: "Theme",
@@ -134,7 +185,7 @@ window.__ModuleLoader__.load({
134
185
  descColor: "Drag the wheel or type exact values — the whole UI follows live",
135
186
  descInterface: "Tune opacity and blur per surface to build depth",
136
187
  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",
188
+ descProfile: "Apply a one-click preset, save multiple named profiles, schedule a day/night switch, or export/import the full theme",
138
189
  colorTitle: "Theme color",
139
190
  colorHint: "Pick hue on the outer ring, adjust saturation & lightness in the square; double-click a slider to reset",
140
191
  swatchTitle: "Quick swatches",
@@ -226,7 +277,58 @@ window.__ModuleLoader__.load({
226
277
  presetNoise: "Flowing noise",
227
278
  presetDots: "Dots",
228
279
  presetWaves: "Waves",
229
- presetPoly: "Low poly"
280
+ presetPoly: "Low poly",
281
+ schemeTitle: "Interface scheme",
282
+ schemeLight: "Light",
283
+ schemeDark: "Dark",
284
+ schemeAuto: "Auto",
285
+ 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",
286
+ presetGalleryTitle: "Appearance presets",
287
+ presetName_default: "Default",
288
+ presetDesc_default: "Restore the plugin's original look",
289
+ presetName_glass: "Frosted glass",
290
+ presetDesc_glass: "Translucent surfaces + frosted depth",
291
+ presetName_minimal: "Minimal",
292
+ presetDesc_minimal: "Near-solid surfaces, no frost",
293
+ presetName_midnight: "Midnight",
294
+ presetDesc_midnight: "Deep violet accent, heavy frost",
295
+ presetName_cyber: "Cyber",
296
+ presetDesc_cyber: "Vivid magenta, vanishing surfaces",
297
+ presetName_warm: "Warm daylight",
298
+ presetDesc_warm: "Amber accent on solid light surfaces",
299
+ presetApplied: "Preset applied",
300
+ profilesTitle: "Saved profiles",
301
+ profileSave: "Save current",
302
+ profileNamePlaceholder: "Profile name…",
303
+ profileNameRequired: "Enter a name first",
304
+ profileSaved: "Profile saved",
305
+ profileApplied: "Profile applied",
306
+ profileApply: "Apply",
307
+ profileDelete: "Delete profile",
308
+ profileDeleteConfirm: "Sure?",
309
+ profileNone: "None",
310
+ profilesEmpty: "No saved profiles yet. Tune the look, then hit \"Save current\" to switch back anytime.",
311
+ scheduleTitle: "Day/night auto switch",
312
+ scheduleHint: "Automatically swap between two saved profiles by clock time or the OS dark mode (colors and surfaces switch; the wallpaper stays)",
313
+ scheduleMode: "Trigger",
314
+ scheduleModeTime: "Fixed times",
315
+ scheduleModeSystem: "Follow system",
316
+ scheduleTimes: "Switch times",
317
+ scheduleDayStart: "Day starts",
318
+ scheduleNightStart: "Night starts",
319
+ scheduleProfiles: "Profiles",
320
+ scheduleDayProfile: "Day",
321
+ scheduleNightProfile: "Night",
322
+ rotTitle: "Wallpaper rotation",
323
+ rotHint: "Add images to a rotation pool and let the wallpaper change on your cadence; takes effect immediately when enabled",
324
+ rotAdd: "Add images",
325
+ rotRemove: "Remove",
326
+ rotShuffle: "Shuffle",
327
+ rotOrder: "In order",
328
+ rotReload: "Every refresh",
329
+ rotDaily: "Daily",
330
+ rotWeekly: "Weekly",
331
+ rotNow: "Switch now"
230
332
  };
231
333
  //#endregion
232
334
  //#region src/client/state.ts
@@ -270,7 +372,26 @@ window.__ModuleLoader__.load({
270
372
  generatedBg: null,
271
373
  regenerateOnReload: false,
272
374
  chatTextOpacity: 0,
273
- trajectoryOpacity: 1
375
+ trajectoryOpacity: 1,
376
+ profiles: [],
377
+ rotation: {
378
+ enabled: false,
379
+ mode: "shuffle",
380
+ interval: "daily",
381
+ current: 0,
382
+ items: [],
383
+ lastRotate: null
384
+ },
385
+ schedule: {
386
+ enabled: false,
387
+ mode: "time",
388
+ dayProfile: null,
389
+ nightProfile: null,
390
+ dayStart: "07:00",
391
+ nightStart: "19:00"
392
+ },
393
+ schemeOverride: "auto",
394
+ activeProfile: null
274
395
  };
275
396
  const clamp01 = (n, def) => typeof n === "number" ? Math.min(1, Math.max(0, n)) : def;
276
397
  let cfg = {
@@ -403,8 +524,144 @@ window.__ModuleLoader__.load({
403
524
  function rVideoBgState() {
404
525
  return cfg.videoBgState;
405
526
  }
527
+ function rProfiles() {
528
+ return Array.isArray(cfg.profiles) ? cfg.profiles : [];
529
+ }
530
+ function rRotation() {
531
+ const r = cfg.rotation;
532
+ return r && typeof r === "object" ? {
533
+ ...DEFAULT_CONFIG.rotation,
534
+ ...r,
535
+ items: Array.isArray(r.items) ? r.items : []
536
+ } : {
537
+ ...DEFAULT_CONFIG.rotation,
538
+ items: []
539
+ };
540
+ }
541
+ function rSchedule() {
542
+ return cfg.schedule && typeof cfg.schedule === "object" ? {
543
+ ...DEFAULT_CONFIG.schedule,
544
+ ...cfg.schedule
545
+ } : { ...DEFAULT_CONFIG.schedule };
546
+ }
547
+ function rSchemeOverride() {
548
+ return cfg.schemeOverride === "light" || cfg.schemeOverride === "dark" ? cfg.schemeOverride : "auto";
549
+ }
550
+ /** Effective interface scheme (drives data-ds-dark-theme / color-scheme): a
551
+ * forced override wins; in auto a picked color decides through its palette
552
+ * direction (keeps native controls aligned with the drawn surfaces), and
553
+ * without a pick the background's brightness verdict does. Stay light as the
554
+ * last fallback. */
555
+ function rScheme() {
556
+ const o = rSchemeOverride();
557
+ if (o !== "auto") return o;
558
+ if (rHasColor()) return rColorScheme();
559
+ return rBgDark() ? "dark" : "light";
560
+ }
561
+ /** Direction the color palette itself is built in: a forced override wins; in
562
+ * auto the theme color's own lightness decides (0.2.4 behavior) so surfaces
563
+ * always contrast with the palette's label fonts — the wallpaper verdict must
564
+ * not drag a dark color's surfaces into a light build (and vice versa) or
565
+ * text and surfaces converge. */
566
+ function rColorScheme() {
567
+ const o = rSchemeOverride();
568
+ if (o !== "auto") return o;
569
+ return rHasColor() && rColor()[2] < .55 ? "dark" : "light";
570
+ }
571
+ /** Snapshot of the appearance fields a profile/preset restores. */
572
+ function currentAppearance() {
573
+ return {
574
+ color: cfg.color,
575
+ opacities: { ...rOps() },
576
+ blurs: { ...rBlurs() },
577
+ settingsOpacity: rSop(),
578
+ wallpaperOpacity: rWop(),
579
+ blur: rBl(),
580
+ chatTextOpacity: rChatTextOpacity(),
581
+ trajectoryOpacity: rTrajectoryOpacity()
582
+ };
583
+ }
584
+ /** Apply an appearance snapshot onto cfg (meta fields untouched). */
585
+ function applyAppearance(ap) {
586
+ cfg.color = Array.isArray(ap.color) && ap.color.length === 3 ? [...ap.color] : null;
587
+ cfg.opacities = {
588
+ ...DEFAULT_CONFIG.opacities,
589
+ ...ap.opacities ?? {}
590
+ };
591
+ cfg.blurs = {
592
+ ...DEFAULT_CONFIG.blurs,
593
+ ...ap.blurs ?? {}
594
+ };
595
+ cfg.settingsOpacity = clamp01(ap.settingsOpacity, DEFAULT_CONFIG.settingsOpacity);
596
+ cfg.wallpaperOpacity = clamp01(ap.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity);
597
+ cfg.blur = typeof ap.blur === "number" ? Math.min(60, Math.max(0, ap.blur)) : DEFAULT_CONFIG.blur;
598
+ cfg.chatTextOpacity = clamp01(ap.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity);
599
+ cfg.trajectoryOpacity = clamp01(ap.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity);
600
+ }
406
601
  const num = (n, def) => typeof n === "number" ? n : def;
407
602
  const cl = (n, lo, hi, def) => typeof n === "number" ? Math.min(hi, Math.max(lo, n)) : def;
603
+ const HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
604
+ function adoptProfiles(raw) {
605
+ if (!Array.isArray(raw)) return [];
606
+ const out = [];
607
+ for (const item of raw.slice(0, 20)) {
608
+ const p = item ?? {};
609
+ if (typeof p.id !== "string" || p.id.length === 0 || p.id.length > 64) continue;
610
+ if (out.some((e) => e.id === p.id)) continue;
611
+ const ac = p.config ?? {};
612
+ out.push({
613
+ id: p.id,
614
+ name: typeof p.name === "string" && p.name.trim() ? p.name.slice(0, 60) : "Profile",
615
+ createdAt: typeof p.createdAt === "string" ? p.createdAt : "",
616
+ config: {
617
+ color: Array.isArray(ac.color) && ac.color.length === 3 ? [...ac.color] : null,
618
+ opacities: {
619
+ ...DEFAULT_CONFIG.opacities,
620
+ ...ac.opacities ?? {}
621
+ },
622
+ blurs: {
623
+ ...DEFAULT_CONFIG.blurs,
624
+ ...ac.blurs ?? {}
625
+ },
626
+ settingsOpacity: clamp01(ac.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
627
+ wallpaperOpacity: clamp01(ac.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity),
628
+ blur: num(ac.blur, DEFAULT_CONFIG.blur),
629
+ chatTextOpacity: clamp01(ac.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
630
+ trajectoryOpacity: clamp01(ac.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity)
631
+ }
632
+ });
633
+ }
634
+ return out;
635
+ }
636
+ function adoptRotation(raw) {
637
+ const r = raw ?? {};
638
+ const items = Array.isArray(r.items) ? r.items.filter((it) => {
639
+ const i = it ?? {};
640
+ return typeof i?.file === "string" && /^[\w-]+\.(jpg|jpeg|png|gif|webp)$/i.test(i.file);
641
+ }).slice(0, 30).map((it) => ({
642
+ file: it.file,
643
+ thumb: typeof it.thumb === "string" && it.thumb.startsWith("data:image/") && it.thumb.length <= 65536 ? it.thumb : ""
644
+ })) : [];
645
+ return {
646
+ enabled: r.enabled === true,
647
+ mode: r.mode === "order" ? "order" : "shuffle",
648
+ interval: r.interval === "reload" || r.interval === "weekly" ? r.interval : "daily",
649
+ current: num(r.current, 0),
650
+ items,
651
+ lastRotate: typeof r.lastRotate === "string" ? r.lastRotate : null
652
+ };
653
+ }
654
+ function adoptSchedule(raw) {
655
+ const r = raw ?? {};
656
+ return {
657
+ enabled: r.enabled === true,
658
+ mode: r.mode === "system" ? "system" : "time",
659
+ dayProfile: typeof r.dayProfile === "string" ? r.dayProfile : null,
660
+ nightProfile: typeof r.nightProfile === "string" ? r.nightProfile : null,
661
+ dayStart: typeof r.dayStart === "string" && HHMM_RE.test(r.dayStart) ? r.dayStart : DEFAULT_CONFIG.schedule.dayStart,
662
+ nightStart: typeof r.nightStart === "string" && HHMM_RE.test(r.nightStart) ? r.nightStart : DEFAULT_CONFIG.schedule.nightStart
663
+ };
664
+ }
408
665
  function adoptBgState(s) {
409
666
  return {
410
667
  zoom: num(s.zoom, 1),
@@ -470,7 +727,12 @@ window.__ModuleLoader__.load({
470
727
  generatedBg: generatedBg ? normalizeGeneratedBg(generatedBg) : null,
471
728
  regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
472
729
  chatTextOpacity: clamp01(c.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
473
- trajectoryOpacity: clamp01(c.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity)
730
+ trajectoryOpacity: clamp01(c.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity),
731
+ profiles: adoptProfiles(c.profiles),
732
+ rotation: adoptRotation(c.rotation),
733
+ schedule: adoptSchedule(c.schedule),
734
+ schemeOverride: c.schemeOverride === "light" || c.schemeOverride === "dark" ? c.schemeOverride : "auto",
735
+ activeProfile: typeof c.activeProfile === "string" ? c.activeProfile : null
474
736
  };
475
737
  }
476
738
  function normalizeGeneratedBg(p) {
@@ -548,7 +810,9 @@ window.__ModuleLoader__.load({
548
810
  function persistConfig() {
549
811
  rpcCall("writeConfig", { config: cfg });
550
812
  }
551
- /** Load the persisted theme (config + wallpaper + video URL) from the node half. */
813
+ /** Load the persisted theme (config + wallpaper + video URL) from the node half.
814
+ * Resolves true when the server advanced a due wallpaper rotation during the
815
+ * read — the restored wallpaper is then already the new pick. */
552
816
  async function loadPersisted() {
553
817
  const data = await rpcCall("read", {});
554
818
  if (data && typeof data === "object") {
@@ -559,7 +823,9 @@ window.__ModuleLoader__.load({
559
823
  if (typeof d.videoUrl === "string") setWpVideoUrl(d.videoUrl, cfg.videoMime);
560
824
  else if (d.videoUrl === null) setWpVideoUrl(null, null);
561
825
  if (cfg.backgroundType === "image") setWpUrl(d.wallpaper === null ? null : d.wallpaper);
826
+ return d.rotated === true;
562
827
  }
828
+ return false;
563
829
  }
564
830
  /** Persist a wallpaper (null removes it); one-shot, no debounce. */
565
831
  function persistWallpaper(dataUrl) {
@@ -618,6 +884,79 @@ window.__ModuleLoader__.load({
618
884
  return false;
619
885
  }
620
886
  }
887
+ /** Add an image (data URL + small thumbnail) to the server-side rotation pool. */
888
+ async function rotationAdd(dataUrl, thumb) {
889
+ if (!rpcCallFn) return {
890
+ ok: false,
891
+ error: "rpc not ready"
892
+ };
893
+ try {
894
+ const res = await rpcCallFn(rpcEndpoint("rotationAdd"), {
895
+ dataUrl,
896
+ thumb
897
+ });
898
+ if (res && res.ok === true) return res.value ?? {
899
+ ok: false,
900
+ error: "no value"
901
+ };
902
+ return {
903
+ ok: false,
904
+ error: res?.error?.message ?? "request failed"
905
+ };
906
+ } catch (e) {
907
+ return {
908
+ ok: false,
909
+ error: e instanceof Error ? e.message : String(e)
910
+ };
911
+ }
912
+ }
913
+ /** Remove a rotation item by index. Returns the updated items list. */
914
+ async function rotationRemove(index) {
915
+ if (!rpcCallFn) return {
916
+ ok: false,
917
+ error: "rpc not ready"
918
+ };
919
+ try {
920
+ const res = await rpcCallFn(rpcEndpoint("rotationRemove"), { index });
921
+ if (res && res.ok === true) return res.value ?? {
922
+ ok: false,
923
+ error: "no value"
924
+ };
925
+ return {
926
+ ok: false,
927
+ error: res?.error?.message ?? "request failed"
928
+ };
929
+ } catch (e) {
930
+ return {
931
+ ok: false,
932
+ error: e instanceof Error ? e.message : String(e)
933
+ };
934
+ }
935
+ }
936
+ /** Activate a rotation item: the server copies its bytes into the wallpaper
937
+ * slot and returns the freshly sniffed data URL for immediate display. */
938
+ async function rotationActivate(index) {
939
+ if (!rpcCallFn) return {
940
+ ok: false,
941
+ error: "rpc not ready"
942
+ };
943
+ try {
944
+ const res = await rpcCallFn(rpcEndpoint("rotationSet"), { index });
945
+ if (res && res.ok === true) return res.value ?? {
946
+ ok: false,
947
+ error: "no value"
948
+ };
949
+ return {
950
+ ok: false,
951
+ error: res?.error?.message ?? "request failed"
952
+ };
953
+ } catch (e) {
954
+ return {
955
+ ok: false,
956
+ error: e instanceof Error ? e.message : String(e)
957
+ };
958
+ }
959
+ }
621
960
  //#endregion
622
961
  //#region src/client/utils/color.ts
623
962
  function hsvToHsl(h, s, v) {
@@ -639,20 +978,23 @@ window.__ModuleLoader__.load({
639
978
  let tokensCacheKey = "";
640
979
  let tokensCache = null;
641
980
  /**
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.
981
+ * Memoized token generation: the same (hue, sat, lit, scheme) input always
982
+ * yields the same token set, and applyWp / applyCustomTokens /
983
+ * applySettingsOverrides call this repeatedly (slider drags, viewport
984
+ * re-applies), so cache the last result and skip the 30+ hsl() string builds
985
+ * when nothing changed. The optional scheme forces the palette direction
986
+ * (light/dark) independently of the color's own lightness.
646
987
  */
647
- function genTokens(hue, sat, lit) {
648
- const key = `${hue}|${sat}|${lit}`;
988
+ function genTokens(hue, sat, lit, scheme) {
989
+ const key = `${hue}|${sat}|${lit}|${scheme ?? "auto"}`;
649
990
  if (tokensCacheKey === key && tokensCache) return tokensCache;
650
991
  tokensCacheKey = key;
651
- tokensCache = buildTokens(hue, sat, lit);
992
+ tokensCache = buildTokens(hue, sat, lit, scheme);
652
993
  return tokensCache;
653
994
  }
654
- function buildTokens(hue, sat, lit) {
655
- const dark = lit < .55;
995
+ function buildTokens(hue, sat, lit, scheme) {
996
+ const dark = scheme !== void 0 ? scheme === "dark" : lit < .55;
997
+ 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
998
  const h = (d) => ((hue + d) % 360 + 360) % 360;
657
999
  const s = (d) => Math.max(0, Math.min(1, sat + d));
658
1000
  const l = (d) => Math.max(0, Math.min(1, lit + d));
@@ -861,7 +1203,7 @@ window.__ModuleLoader__.load({
861
1203
  const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
862
1204
  const v = max / 255;
863
1205
  const s = max === 0 ? 0 : (max - min) / max;
864
- totalLum += v;
1206
+ totalLum += (.2126 * r + .7152 * gg + .0722 * b) / 255;
865
1207
  sampled++;
866
1208
  if (s < .08 || v < .12 || v > .97) continue;
867
1209
  const key = r >> 4 << 8 | gg >> 4 << 4 | b >> 4;
@@ -1589,10 +1931,20 @@ window.__ModuleLoader__.load({
1589
1931
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1590
1932
  appliedTokenNames = [];
1591
1933
  }
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. */
1934
+ /** Drop every applied custom token and forget the token fingerprint, so a
1935
+ * later color-less profile (system theme) leaves no stale rule behind. */
1936
+ function clearThemeTokens() {
1937
+ clearCustomTokens();
1938
+ baseTokenKey = "";
1939
+ lastBgKey = "";
1940
+ document.body.removeAttribute("data-ds-dark-theme");
1941
+ document.body.style.removeProperty("color-scheme");
1942
+ }
1943
+ /** Label tokens flipped by the background brightness verdict. The faint tiers
1944
+ * (caption/dimmed) are deliberately NOT flipped: they back placeholder/hint
1945
+ * text, which must stay visibly weaker than real input even when the wallpaper
1946
+ * brightness flips the main label direction. Exported so the skin registration
1947
+ * (index) can flip fonts through the host theme service too. */
1596
1948
  const LABEL_TOKENS = [
1597
1949
  "--dsw-alias-label-primary",
1598
1950
  "--dsw-alias-label-secondary",
@@ -1645,19 +1997,41 @@ window.__ModuleLoader__.load({
1645
1997
  });
1646
1998
  }
1647
1999
  let lastBgKey = "";
2000
+ /** Palette source for the token rule: the picked color; a forced scheme
2001
+ * without a picked color builds a neutral near-gray palette in that
2002
+ * direction; auto without a color keeps the host's own palette (null — only
2003
+ * the label direction gets asserted). The palette direction follows the
2004
+ * color's own lightness (rColorScheme), not the wallpaper verdict, so the
2005
+ * surfaces keep contrasting with the fonts. */
2006
+ function paletteTokens() {
2007
+ const scheme = rColorScheme();
2008
+ if (rHasColor()) {
2009
+ const [h, s, l] = rColor();
2010
+ return genTokens(h, s, l, scheme).tokens;
2011
+ }
2012
+ if (rSchemeOverride() !== "auto") return genTokens(220, .04, scheme === "dark" ? .14 : .92, scheme).tokens;
2013
+ return null;
2014
+ }
1648
2015
  function applyCustomTokensNow(ops) {
2016
+ const hasColor = rHasColor();
2017
+ const override = rSchemeOverride();
1649
2018
  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";
2019
+ const scheme = rScheme();
2020
+ const verdict = rBgDark();
2021
+ const palette = paletteTokens();
2022
+ const tokens = { ...palette };
2023
+ if (override === "auto") {
2024
+ const fontDark = hasColor ? rColorScheme() === "dark" : verdict;
2025
+ if (fontDark !== null && fontDark !== void 0) {
2026
+ const font = fontDark ? "#fff" : "#000";
1656
2027
  for (const name of LABEL_TOKENS) tokens[name] = font;
1657
2028
  }
1658
- const forceDark = dark ?? l < .55;
1659
- if (`${h}|${s}|${l}|${dark}` !== baseTokenKey) {
1660
- baseTokenKey = `${h}|${s}|${l}|${dark}`;
2029
+ }
2030
+ try {
2031
+ const forceDark = scheme === "dark";
2032
+ const key = `${h}|${s}|${l}|${verdict}|${scheme}|${hasColor}`;
2033
+ if (key !== baseTokenKey) {
2034
+ baseTokenKey = key;
1661
2035
  if (forceDark) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
1662
2036
  else document.body.removeAttribute("data-ds-dark-theme");
1663
2037
  const decls = [`color-scheme:${forceDark ? "dark" : "light"}`];
@@ -1669,6 +2043,7 @@ window.__ModuleLoader__.load({
1669
2043
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1670
2044
  appliedTokenNames = Object.keys(tokens);
1671
2045
  }
2046
+ if (palette === null) return;
1672
2047
  const root = document.documentElement;
1673
2048
  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
2049
  root.style.setProperty("--dsh-any-op-menu-cordis", toRgba(tokens["--dsw-specific-menu"] ?? "#000", ops.input));
@@ -1688,7 +2063,7 @@ window.__ModuleLoader__.load({
1688
2063
  }
1689
2064
  function applySettingsOverrides(op) {
1690
2065
  const [h, s, l] = rColor();
1691
- const tokens = genTokens(h, s, l).tokens;
2066
+ const tokens = genTokens(h, s, l, rColorScheme()).tokens;
1692
2067
  const layer1 = tokens["--dsw-alias-bg-layer-1"];
1693
2068
  const layer2 = tokens["--dsw-alias-bg-layer-2"];
1694
2069
  const layer3 = tokens["--dsw-alias-bg-layer-3"];
@@ -1700,7 +2075,7 @@ window.__ModuleLoader__.load({
1700
2075
  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
2076
  function applyTrajectoryOverrides(op) {
1702
2077
  const [h, s, l] = rColor();
1703
- const tokens = genTokens(h, s, l).tokens;
2078
+ const tokens = genTokens(h, s, l, rColorScheme()).tokens;
1704
2079
  const layer1 = tokens["--dsw-alias-bg-layer-1"];
1705
2080
  const layer2 = tokens["--dsw-alias-bg-layer-2"];
1706
2081
  const layer3 = tokens["--dsw-alias-bg-layer-3"];
@@ -1708,6 +2083,14 @@ window.__ModuleLoader__.load({
1708
2083
  if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-2", toRgba(layer2, op));
1709
2084
  if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-3", toRgba(layer3, op));
1710
2085
  }
2086
+ /** Register a callback fired when a wallpaper-extracted theme color is
2087
+ * adopted by the auto-adaptation path, so the section can re-register the
2088
+ * host skin, sync the editor UI and persist the pick (wallpaper.ts cannot
2089
+ * do those itself — they live in the section). */
2090
+ let colorAdoptedListener = null;
2091
+ function onColorAdopted(cb) {
2092
+ colorAdoptedListener = cb;
2093
+ }
1711
2094
  /** Apply the theme color: use the saved pick directly, or fall back to
1712
2095
  * extracting a dominant color from the current wallpaper. */
1713
2096
  function applyThemeColor() {
@@ -1717,7 +2100,10 @@ window.__ModuleLoader__.load({
1717
2100
  }
1718
2101
  const url = rWp();
1719
2102
  if (url) extractWallpaperColor(url, cfg.backgroundType === "video" ? rVideoBgState() : rBgState()).then((hsl) => {
1720
- if (hsl) cfg.color = hsl;
2103
+ if (hsl) {
2104
+ cfg.color = hsl;
2105
+ colorAdoptedListener?.(hsl);
2106
+ }
1721
2107
  applyWp();
1722
2108
  });
1723
2109
  else applyWp();
@@ -1782,7 +2168,7 @@ window.__ModuleLoader__.load({
1782
2168
  setBgDark(null);
1783
2169
  analyzeFrameDark(frame).then((dark) => {
1784
2170
  if (dark === null || wpController !== controller) return;
1785
- setBgDark(dark);
2171
+ applyVerdict(dark);
1786
2172
  applyCustomTokens(rOps());
1787
2173
  });
1788
2174
  });
@@ -1848,11 +2234,11 @@ window.__ModuleLoader__.load({
1848
2234
  * reducing the main-bg opacity stacked a second alpha onto the sidebar; moving
1849
2235
  * the alpha onto the columns keeps the sidebar owned by its own slider. */
1850
2236
  function applyPartOpacities(ops) {
1851
- if (!(rHasColor() || rBgDark() !== null)) return;
2237
+ const tokens = paletteTokens();
2238
+ if (tokens === null) return;
1852
2239
  discoverParts();
1853
2240
  if (frameEl === null) return;
1854
- const [h, s, l] = rColor();
1855
- const base = genTokens(h, s, l).tokens["--dsw-alias-bg-base"];
2241
+ const base = tokens["--dsw-alias-bg-base"];
1856
2242
  frameEl.style.background = "transparent";
1857
2243
  if (centerEl !== null) centerEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
1858
2244
  if (detailsEl !== null) detailsEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
@@ -2071,7 +2457,7 @@ window.__ModuleLoader__.load({
2071
2457
  discoverParts();
2072
2458
  if (centerEl === null) return;
2073
2459
  const [h, s, l] = rColor();
2074
- const surface = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-1"];
2460
+ const surface = genTokens(h, s, l, rColorScheme()).tokens["--dsw-alias-bg-layer-1"];
2075
2461
  VIEW_CARDS.forEach((spec, i) => {
2076
2462
  const target = discoverViewTarget(i, spec);
2077
2463
  if (target === null) return;
@@ -2114,8 +2500,7 @@ window.__ModuleLoader__.load({
2114
2500
  let themeObserver = null;
2115
2501
  let themeRaf = 0;
2116
2502
  function reassertScheme() {
2117
- const [, , l] = rColor();
2118
- if (rBgDark() ?? l < .55) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
2503
+ if (rScheme() === "dark") document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
2119
2504
  else document.body.removeAttribute("data-ds-dark-theme");
2120
2505
  applyCustomTokens(rOps());
2121
2506
  }
@@ -2126,7 +2511,7 @@ window.__ModuleLoader__.load({
2126
2511
  if (themeObserver !== null || typeof MutationObserver === "undefined") return () => void 0;
2127
2512
  themeObserver = new MutationObserver(() => {
2128
2513
  if (document.body.getAttribute("data-ds-dark-theme") === "dsh-any-background") return;
2129
- if (!(rHasColor() || rBgDark() !== null)) return;
2514
+ if (!(rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto")) return;
2130
2515
  if (themeRaf !== 0) return;
2131
2516
  themeRaf = requestAnimationFrame(() => {
2132
2517
  themeRaf = 0;
@@ -2253,6 +2638,38 @@ window.__ModuleLoader__.load({
2253
2638
  if (dragLow) setDragLow(false);
2254
2639
  };
2255
2640
  }
2641
+ let wpVerdict = null;
2642
+ let verdictListener = null;
2643
+ /** Register a callback fired when the background brightness verdict CHANGES
2644
+ * (a new wallpaper was analyzed, a generated bg regenerated), so the skin can
2645
+ * be re-registered through the host theme service. */
2646
+ function onVerdictApplied(cb) {
2647
+ verdictListener = cb;
2648
+ }
2649
+ function applyVerdict(dark) {
2650
+ if (rBgDark() === dark) return;
2651
+ setBgDark(dark);
2652
+ if (dark !== null) verdictListener?.();
2653
+ }
2654
+ function updateWpVerdict(url) {
2655
+ if (url === null) {
2656
+ applyVerdict(null);
2657
+ return;
2658
+ }
2659
+ if (wpVerdict !== null && wpVerdict.url === url) {
2660
+ applyVerdict(wpVerdict.dark);
2661
+ return;
2662
+ }
2663
+ analyzeFrameDark(url).then((dark) => {
2664
+ if (dark === null) return;
2665
+ wpVerdict = {
2666
+ url,
2667
+ dark
2668
+ };
2669
+ applyVerdict(dark);
2670
+ applyCustomTokens(rOps());
2671
+ });
2672
+ }
2256
2673
  function applyImageWp(url) {
2257
2674
  clearDynamicBg();
2258
2675
  clearVideoEl();
@@ -2299,6 +2716,7 @@ window.__ModuleLoader__.load({
2299
2716
  }
2300
2717
  captureLowRes(url, () => void 0);
2301
2718
  applyWpEffects();
2719
+ updateWpVerdict(url);
2302
2720
  }
2303
2721
  /** Video wallpaper: a muted looping <video> inside the wallpaper layer.
2304
2722
  * Placement modes map onto object-fit (tile has no video equivalent and
@@ -2335,6 +2753,7 @@ window.__ModuleLoader__.load({
2335
2753
  videoEl.style.objectFit = mode === "stretch" ? "fill" : mode === "fill" || mode === "tile" ? "cover" : "contain";
2336
2754
  }
2337
2755
  applyWpEffects();
2756
+ updateWpVerdict(rWp());
2338
2757
  }
2339
2758
  function applyWpEffects() {
2340
2759
  if (!wpEl) return;
@@ -2368,8 +2787,10 @@ window.__ModuleLoader__.load({
2368
2787
  clearVideoEl();
2369
2788
  wpEl?.remove();
2370
2789
  wpEl = null;
2790
+ wpVerdict = null;
2791
+ setBgDark(null);
2371
2792
  }
2372
- if (rHasColor() || rBgDark() !== null) applyCustomTokens(rOps());
2793
+ if (rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto") applyCustomTokens(rOps());
2373
2794
  if (rHasColor()) {
2374
2795
  applySettingsOverrides(rSop());
2375
2796
  applyTrajectoryOverrides(rTrajectoryOpacity());
@@ -2381,6 +2802,7 @@ window.__ModuleLoader__.load({
2381
2802
  clearVideoEl();
2382
2803
  disposeVideoObjectUrl();
2383
2804
  setBgDark(null);
2805
+ wpVerdict = null;
2384
2806
  wpEl?.remove();
2385
2807
  wpEl = null;
2386
2808
  clearCustomTokens();
@@ -2490,6 +2912,56 @@ window.__ModuleLoader__.load({
2490
2912
  v.src = url;
2491
2913
  });
2492
2914
  }
2915
+ //#endregion
2916
+ //#region src/client/utils/image.ts
2917
+ /**
2918
+ * Read a chosen image file as a data URL WITHOUT re-encoding: the original
2919
+ * pixels are kept as-is (no canvas downscale / JPEG re-compression), so the
2920
+ * wallpaper is stored and displayed at full fidelity. The tradeoff is a larger
2921
+ * payload over the RPC channel and on disk for big images.
2922
+ */
2923
+ function readImg(file, cb) {
2924
+ const r = new FileReader();
2925
+ r.onerror = () => cb(null);
2926
+ r.onload = () => cb(r.result);
2927
+ r.readAsDataURL(file);
2928
+ }
2929
+ /** Promised readImg variant for async flows. */
2930
+ function readImgAsync(file) {
2931
+ return new Promise((resolve) => readImg(file, resolve));
2932
+ }
2933
+ /** Build a small JPEG thumbnail (rotation-pool picker preview). Resolves null
2934
+ * when the image cannot be decoded. */
2935
+ function makeThumb(dataUrl, maxSide = 96) {
2936
+ return new Promise((resolve) => {
2937
+ const img = new Image();
2938
+ img.onerror = () => resolve(null);
2939
+ img.onload = () => {
2940
+ try {
2941
+ const iw = img.naturalWidth || img.width;
2942
+ const ih = img.naturalHeight || img.height;
2943
+ if (iw <= 0 || ih <= 0) {
2944
+ resolve(null);
2945
+ return;
2946
+ }
2947
+ const k = Math.min(1, maxSide / Math.max(iw, ih));
2948
+ const c = document.createElement("canvas");
2949
+ c.width = Math.max(1, Math.round(iw * k));
2950
+ c.height = Math.max(1, Math.round(ih * k));
2951
+ const g = c.getContext("2d");
2952
+ if (!g) {
2953
+ resolve(null);
2954
+ return;
2955
+ }
2956
+ g.drawImage(img, 0, 0, c.width, c.height);
2957
+ resolve(c.toDataURL("image/jpeg", .72));
2958
+ } catch {
2959
+ resolve(null);
2960
+ }
2961
+ };
2962
+ img.src = dataUrl;
2963
+ });
2964
+ }
2493
2965
  const UI_CSS = `
2494
2966
  /* The section is rendered INLINE inside the host settings dialog's content
2495
2967
  * column: the host provides the modal chrome (backdrop, centering, closing).
@@ -2651,6 +3123,43 @@ window.__ModuleLoader__.load({
2651
3123
  .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
3124
  .dab-footer-mono{font-family:var(--dab-mono);letter-spacing:.02em}
2653
3125
 
3126
+ /* ── presets / profiles / schedule / rotation ────────────────────────────── */
3127
+ .dab-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
3128
+ .dab-preset-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
3129
+ .dab-preset{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:12px;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:13px;cursor:pointer;font:inherit;text-align:left;transition:border-color .25s,transform .25s,box-shadow .25s}
3130
+ .dab-preset:hover{transform:translateY(-2px);border-color:var(--dsw-alias-brand-primary);box-shadow:0 8px 22px -12px rgba(0,0,0,.3)}
3131
+ .dab-preset-dot{width:22px;height:22px;border-radius:50%;margin-bottom:4px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.12),0 4px 10px -4px rgba(0,0,0,.35);background:var(--dsw-alias-bg-layer-2);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary)}
3132
+ .dab-preset-name{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary)}
3133
+ .dab-preset-desc{font-size:10.5px;line-height:1.45;color:var(--dsw-alias-label-tertiary)}
3134
+ .dab-profile-list{display:flex;flex-direction:column;gap:8px;margin-top:12px}
3135
+ .dab-profile-row{display:flex;align-items:center;gap:11px;padding:9px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2);transition:border-color .22s ease}
3136
+ .dab-profile-row.is-active{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent)}
3137
+ .dab-profile-dot{width:22px;height:22px;flex:none;border-radius:50%;background:var(--dsw-alias-bg-layer-1);box-shadow:inset 0 0 0 1px rgba(0,0,0,.12);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary)}
3138
+ .dab-profile-meta{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
3139
+ .dab-profile-name{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3140
+ .dab-profile-sub{font-family:var(--dab-mono);font-size:10px;color:var(--dsw-alias-label-tertiary)}
3141
+ .dab-btn-confirm{background:var(--dsw-alias-state-error-primary)!important;color:#fff!important;border-color:transparent!important}
3142
+ .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}
3143
+ .dab-select:focus{border-color:var(--dsw-alias-brand-primary)}
3144
+ .dab-timeinput{height:32px;padding:0 8px;border-radius:9px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-family:var(--dab-mono);font-size:12px;outline:none}
3145
+ .dab-timeinput:focus{border-color:var(--dsw-alias-brand-primary)}
3146
+ .dab-schedule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-top:12px}
3147
+ .dab-schedule-cell{min-width:0}
3148
+ .dab-time-row{display:flex;flex-direction:column;gap:9px}
3149
+ .dab-time-label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
3150
+ .dab-time-label svg{color:var(--dsw-alias-brand-primary);flex:none}
3151
+ .dab-time-label .dab-select,.dab-time-label .dab-timeinput{flex:1;min-width:0}
3152
+ .dab-thumbstrip{display:flex;flex-wrap:wrap;gap:9px;margin-top:13px}
3153
+ .dab-thumb{position:relative;width:64px;height:44px;border-radius:9px;overflow:hidden;border:1.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:grid;place-items:center;color:var(--dsw-alias-label-tertiary)}
3154
+ .dab-thumb.is-current{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,transparent)}
3155
+ .dab-thumb img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
3156
+ .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}
3157
+ .dab-thumb:hover .dab-thumb-x{opacity:1}
3158
+ .dab-thumb-x:hover{background:var(--dsw-alias-state-error-primary)}
3159
+ .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}
3160
+ .dab-thumb-add:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
3161
+ .dab-chip-sep{width:1px;height:18px;background:var(--dsw-alias-border-l2);margin:0 4px;flex:none}
3162
+
2654
3163
  /* ── toast ───────────────────────────────────────────────────────────────── */
2655
3164
  .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
3165
  .dab-toast-ok{color:var(--dsw-alias-brand-primary);display:grid;place-items:center}
@@ -2958,6 +3467,30 @@ window.__ModuleLoader__.load({
2958
3467
  rx: "2"
2959
3468
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.6v2M7 8h2" })]
2960
3469
  });
3470
+ const PlusIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3471
+ size,
3472
+ className,
3473
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 3v10M3 8h10" })
3474
+ });
3475
+ const MoonIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3476
+ size,
3477
+ className,
3478
+ 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" })
3479
+ });
3480
+ const ClockIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
3481
+ size,
3482
+ className,
3483
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3484
+ cx: "8",
3485
+ cy: "8",
3486
+ r: "5.8"
3487
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 4.8V8l2.2 1.6" })]
3488
+ });
3489
+ const XIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
3490
+ size,
3491
+ className,
3492
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
3493
+ });
2961
3494
  //#endregion
2962
3495
  //#region src/client/components/ErrorBoundary.tsx
2963
3496
  /**
@@ -3600,9 +4133,10 @@ window.__ModuleLoader__.load({
3600
4133
  return "#" + rgb.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
3601
4134
  }
3602
4135
  function ColorPage({ p, notify }) {
3603
- const { t, hue, sat, lit, setColor, extractColor, useStore } = p;
4136
+ const { t, hue, sat, lit, setColor, extractColor, setSchemeOverride, useStore } = p;
3604
4137
  const store = useStore((s) => s);
3605
4138
  const storeUrl = store.url;
4139
+ const schemeOverride = store.schemeOverride;
3606
4140
  const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
3607
4141
  const [extracting, setExtracting] = (0, react.useState)(false);
3608
4142
  const wheel = store.color ?? [
@@ -3748,6 +4282,49 @@ window.__ModuleLoader__.load({
3748
4282
  })
3749
4283
  })]
3750
4284
  }),
4285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4286
+ className: "dab-card dab-rise",
4287
+ style: { "--d": 4 },
4288
+ children: [
4289
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4290
+ className: "dab-swatch-title",
4291
+ children: t("schemeTitle")
4292
+ }),
4293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4294
+ className: "dab-seg",
4295
+ style: { "--w": "86px" },
4296
+ children: [
4297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4298
+ className: "dab-seg-thumb",
4299
+ style: { transform: `translateX(${schemeOverride === "light" ? 0 : schemeOverride === "dark" ? 86 : 172}px)` }
4300
+ }),
4301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4302
+ type: "button",
4303
+ className: `dab-seg-item${schemeOverride === "light" ? " is-active" : ""}`,
4304
+ onClick: () => setSchemeOverride("light"),
4305
+ children: t("schemeLight")
4306
+ }),
4307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4308
+ type: "button",
4309
+ className: `dab-seg-item${schemeOverride === "dark" ? " is-active" : ""}`,
4310
+ onClick: () => setSchemeOverride("dark"),
4311
+ children: t("schemeDark")
4312
+ }),
4313
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4314
+ type: "button",
4315
+ className: `dab-seg-item${schemeOverride === "auto" ? " is-active" : ""}`,
4316
+ onClick: () => setSchemeOverride("auto"),
4317
+ children: t("schemeAuto")
4318
+ })
4319
+ ]
4320
+ }),
4321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4322
+ className: "dab-hint",
4323
+ style: { marginTop: 10 },
4324
+ children: t("schemeHint")
4325
+ })
4326
+ ]
4327
+ }),
3751
4328
  pickerOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorPicker, {
3752
4329
  url: storeUrl,
3753
4330
  t,
@@ -3989,20 +4566,6 @@ window.__ModuleLoader__.load({
3989
4566
  })] });
3990
4567
  }
3991
4568
  //#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
4569
  //#region src/client/components/BgEditor.tsx
4007
4570
  /** Placement slot for the active background type: video framing lives in its
4008
4571
  * own state so image and video edits never overwrite each other. */
@@ -4218,16 +4781,19 @@ window.__ModuleLoader__.load({
4218
4781
  }
4219
4782
  ];
4220
4783
  function BackgroundPage({ p }) {
4221
- const { t, setWp, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, useStore } = p;
4784
+ const { t, setWp, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, setRotation, addRotationItems, removeRotationItem, rotateNow, useStore } = p;
4222
4785
  const store = useStore((s) => s);
4223
4786
  const storeUrl = store.url;
4224
4787
  const backgroundType = store.backgroundType;
4225
4788
  const generatedBg = store.generatedBg;
4226
4789
  const regenerateOnReload = store.regenerateOnReload;
4790
+ const rotation = store.rotation;
4227
4791
  const fileRef = (0, react.useRef)(null);
4792
+ const rotFileRef = (0, react.useRef)(null);
4228
4793
  const [editorOpen, setEditorOpen] = (0, react.useState)(false);
4229
4794
  const [dragOver, setDragOver] = (0, react.useState)(false);
4230
4795
  const [spinTick, setSpinTick] = (0, react.useState)(0);
4796
+ const [rotBusy, setRotBusy] = (0, react.useState)(false);
4231
4797
  const [urlOpen, setUrlOpen] = (0, react.useState)(false);
4232
4798
  const [urlVal, setUrlVal] = (0, react.useState)("");
4233
4799
  const [urlBusy, setUrlBusy] = (0, react.useState)(false);
@@ -4425,7 +4991,7 @@ window.__ModuleLoader__.load({
4425
4991
  ]
4426
4992
  })
4427
4993
  }),
4428
- !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4994
+ !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4429
4995
  className: "dab-card dab-card-hover dab-rise",
4430
4996
  style: { "--d": 3 },
4431
4997
  children: [
@@ -4527,7 +5093,121 @@ window.__ModuleLoader__.load({
4527
5093
  })]
4528
5094
  })
4529
5095
  ]
4530
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5096
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5097
+ className: "dab-card dab-rise",
5098
+ style: { "--d": 4 },
5099
+ children: [
5100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5101
+ className: "dab-row-head",
5102
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5103
+ className: "dab-swatch-title",
5104
+ style: { marginBottom: 0 },
5105
+ children: t("rotTitle")
5106
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5107
+ type: "button",
5108
+ className: `dab-toggle${rotation.enabled ? " is-on" : ""}`,
5109
+ role: "switch",
5110
+ "aria-checked": rotation.enabled,
5111
+ onClick: () => setRotation({ enabled: !rotation.enabled }),
5112
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
5113
+ })]
5114
+ }),
5115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5116
+ className: "dab-hint",
5117
+ style: { marginTop: 8 },
5118
+ children: t("rotHint")
5119
+ }),
5120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5121
+ className: "dab-thumbstrip",
5122
+ children: [rotation.items.map((it, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5123
+ className: `dab-thumb${rotation.enabled && i === rotation.current ? " is-current" : ""}`,
5124
+ children: [it.thumb ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
5125
+ src: it.thumb,
5126
+ alt: "",
5127
+ draggable: false
5128
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhotoIcon, { size: 15 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5129
+ type: "button",
5130
+ className: "dab-thumb-x",
5131
+ title: t("rotRemove"),
5132
+ disabled: rotBusy,
5133
+ onClick: () => {
5134
+ setRotBusy(true);
5135
+ removeRotationItem(i).finally(() => setRotBusy(false));
5136
+ },
5137
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XIcon, { size: 10 })
5138
+ })]
5139
+ }, it.file)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5140
+ type: "button",
5141
+ className: "dab-thumb dab-thumb-add",
5142
+ title: t("rotAdd"),
5143
+ disabled: rotBusy,
5144
+ onClick: () => rotFileRef.current?.click(),
5145
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, { size: 16 })
5146
+ })]
5147
+ }),
5148
+ rotation.items.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5149
+ className: "dab-chip-row",
5150
+ style: { marginTop: 12 },
5151
+ children: [
5152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5153
+ type: "button",
5154
+ className: `dab-chip${rotation.mode === "shuffle" ? " is-active" : ""}`,
5155
+ onClick: () => setRotation({ mode: "shuffle" }),
5156
+ children: t("rotShuffle")
5157
+ }),
5158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5159
+ type: "button",
5160
+ className: `dab-chip${rotation.mode === "order" ? " is-active" : ""}`,
5161
+ onClick: () => setRotation({ mode: "order" }),
5162
+ children: t("rotOrder")
5163
+ }),
5164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-chip-sep" }),
5165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5166
+ type: "button",
5167
+ className: `dab-chip${rotation.interval === "reload" ? " is-active" : ""}`,
5168
+ onClick: () => setRotation({ interval: "reload" }),
5169
+ children: t("rotReload")
5170
+ }),
5171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5172
+ type: "button",
5173
+ className: `dab-chip${rotation.interval === "daily" ? " is-active" : ""}`,
5174
+ onClick: () => setRotation({ interval: "daily" }),
5175
+ children: t("rotDaily")
5176
+ }),
5177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5178
+ type: "button",
5179
+ className: `dab-chip${rotation.interval === "weekly" ? " is-active" : ""}`,
5180
+ onClick: () => setRotation({ interval: "weekly" }),
5181
+ children: t("rotWeekly")
5182
+ }),
5183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5184
+ type: "button",
5185
+ className: "dab-btn",
5186
+ disabled: rotBusy,
5187
+ onClick: () => {
5188
+ setRotBusy(true);
5189
+ rotateNow().finally(() => setRotBusy(false));
5190
+ },
5191
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 13 }), t("rotNow")]
5192
+ })
5193
+ ]
5194
+ }) : null,
5195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5196
+ ref: rotFileRef,
5197
+ type: "file",
5198
+ accept: "image/*",
5199
+ multiple: true,
5200
+ style: { display: "none" },
5201
+ onChange: (e) => {
5202
+ const files = Array.from(e.target.files ?? []);
5203
+ e.target.value = "";
5204
+ if (files.length === 0) return;
5205
+ setRotBusy(true);
5206
+ addRotationItems(files).finally(() => setRotBusy(false));
5207
+ }
5208
+ })
5209
+ ]
5210
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4531
5211
  className: "dab-card dab-rise",
4532
5212
  style: { "--d": 3 },
4533
5213
  children: [
@@ -4692,7 +5372,7 @@ window.__ModuleLoader__.load({
4692
5372
  }),
4693
5373
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4694
5374
  className: "dab-card dab-card-hover dab-rise",
4695
- style: { "--d": 4 },
5375
+ style: { "--d": 5 },
4696
5376
  children: [
4697
5377
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
4698
5378
  label: t("wpOpacity"),
@@ -4764,10 +5444,188 @@ window.__ModuleLoader__.load({
4764
5444
  ] });
4765
5445
  }
4766
5446
  //#endregion
5447
+ //#region src/client/utils/presets.ts
5448
+ const zeroBlurs = {
5449
+ bg: 0,
5450
+ sidebar: 0,
5451
+ card: 0,
5452
+ settings: 0,
5453
+ chat: 0,
5454
+ trajectory: 0,
5455
+ input: 0
5456
+ };
5457
+ const BUILTIN_PRESETS = [
5458
+ {
5459
+ key: "default",
5460
+ appearance: {
5461
+ color: null,
5462
+ opacities: {
5463
+ bg: .85,
5464
+ sidebar: .93,
5465
+ card: 1,
5466
+ input: 1
5467
+ },
5468
+ blurs: { ...zeroBlurs },
5469
+ settingsOpacity: 1,
5470
+ wallpaperOpacity: 1,
5471
+ blur: 0,
5472
+ chatTextOpacity: 0,
5473
+ trajectoryOpacity: 1
5474
+ }
5475
+ },
5476
+ {
5477
+ key: "glass",
5478
+ appearance: {
5479
+ color: [
5480
+ 212,
5481
+ .5,
5482
+ .38
5483
+ ],
5484
+ opacities: {
5485
+ bg: .62,
5486
+ sidebar: .55,
5487
+ card: .62,
5488
+ input: .58
5489
+ },
5490
+ blurs: {
5491
+ ...zeroBlurs,
5492
+ bg: 20,
5493
+ sidebar: 14,
5494
+ card: 12,
5495
+ settings: 20,
5496
+ trajectory: 8,
5497
+ input: 14
5498
+ },
5499
+ settingsOpacity: .88,
5500
+ wallpaperOpacity: 1,
5501
+ blur: 0,
5502
+ chatTextOpacity: 0,
5503
+ trajectoryOpacity: .85
5504
+ }
5505
+ },
5506
+ {
5507
+ key: "minimal",
5508
+ appearance: {
5509
+ color: null,
5510
+ opacities: {
5511
+ bg: .97,
5512
+ sidebar: .97,
5513
+ card: 1,
5514
+ input: 1
5515
+ },
5516
+ blurs: { ...zeroBlurs },
5517
+ settingsOpacity: 1,
5518
+ wallpaperOpacity: 1,
5519
+ blur: 0,
5520
+ chatTextOpacity: 0,
5521
+ trajectoryOpacity: 1
5522
+ }
5523
+ },
5524
+ {
5525
+ key: "midnight",
5526
+ appearance: {
5527
+ color: [
5528
+ 262,
5529
+ .45,
5530
+ .16
5531
+ ],
5532
+ opacities: {
5533
+ bg: .5,
5534
+ sidebar: .45,
5535
+ card: .55,
5536
+ input: .6
5537
+ },
5538
+ blurs: {
5539
+ ...zeroBlurs,
5540
+ bg: 24,
5541
+ sidebar: 18,
5542
+ card: 14,
5543
+ settings: 22,
5544
+ trajectory: 10,
5545
+ input: 16
5546
+ },
5547
+ settingsOpacity: .85,
5548
+ wallpaperOpacity: .92,
5549
+ blur: 2,
5550
+ chatTextOpacity: 0,
5551
+ trajectoryOpacity: .8
5552
+ }
5553
+ },
5554
+ {
5555
+ key: "cyber",
5556
+ appearance: {
5557
+ color: [
5558
+ 315,
5559
+ .7,
5560
+ .22
5561
+ ],
5562
+ opacities: {
5563
+ bg: .42,
5564
+ sidebar: .5,
5565
+ card: .45,
5566
+ input: .55
5567
+ },
5568
+ blurs: {
5569
+ ...zeroBlurs,
5570
+ bg: 16,
5571
+ sidebar: 12,
5572
+ card: 10,
5573
+ settings: 18,
5574
+ trajectory: 6,
5575
+ input: 20
5576
+ },
5577
+ settingsOpacity: .8,
5578
+ wallpaperOpacity: 1,
5579
+ blur: 0,
5580
+ chatTextOpacity: 0,
5581
+ trajectoryOpacity: .78
5582
+ }
5583
+ },
5584
+ {
5585
+ key: "warm",
5586
+ appearance: {
5587
+ color: [
5588
+ 28,
5589
+ .6,
5590
+ .72
5591
+ ],
5592
+ opacities: {
5593
+ bg: .92,
5594
+ sidebar: .95,
5595
+ card: 1,
5596
+ input: 1
5597
+ },
5598
+ blurs: { ...zeroBlurs },
5599
+ settingsOpacity: 1,
5600
+ wallpaperOpacity: 1,
5601
+ blur: 0,
5602
+ chatTextOpacity: 0,
5603
+ trajectoryOpacity: 1
5604
+ }
5605
+ }
5606
+ ];
5607
+ //#endregion
4767
5608
  //#region src/client/components/pages/ProfilePage.tsx
5609
+ const hexOf = (color) => color === null ? "" : "#" + hslToRgb(color[0], color[1], color[2]).map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
5610
+ function ProfileDot({ entry }) {
5611
+ const hex = hexOf(entry.config.color);
5612
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5613
+ className: "dab-profile-dot",
5614
+ style: hex ? { background: hex } : void 0,
5615
+ title: hex ? hex.toUpperCase() : void 0,
5616
+ children: hex ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 11 })
5617
+ });
5618
+ }
4768
5619
  function ProfilePage({ p, notify }) {
4769
- const { t, exportTheme, importTheme } = p;
5620
+ const { t, exportTheme, importTheme, saveProfile, applyProfile, deleteProfile, applyPreset, setSchedule, useStore } = p;
5621
+ const store = useStore((s) => s);
5622
+ const profiles = store.profiles;
5623
+ const schedule = store.schedule;
5624
+ const activeProfile = store.activeProfile;
4770
5625
  const importRef = (0, react.useRef)(null);
5626
+ const [nameOpen, setNameOpen] = (0, react.useState)(false);
5627
+ const [nameVal, setNameVal] = (0, react.useState)("");
5628
+ const [confirmId, setConfirmId] = (0, react.useState)(null);
4771
5629
  const onImport = async (file) => {
4772
5630
  try {
4773
5631
  const ok = await importTheme(file);
@@ -4776,6 +5634,39 @@ window.__ModuleLoader__.load({
4776
5634
  notify(t("importFail"), false);
4777
5635
  }
4778
5636
  };
5637
+ const onSave = () => {
5638
+ const name = nameVal.trim();
5639
+ if (!name) return;
5640
+ if (saveProfile(name)) {
5641
+ notify(t("profileSaved"), true);
5642
+ setNameVal("");
5643
+ setNameOpen(false);
5644
+ } else notify(t("profileNameRequired"), false);
5645
+ };
5646
+ const onApply = (entry) => {
5647
+ if (applyProfile(entry.id)) notify(t("profileApplied"), true);
5648
+ };
5649
+ const onDelete = (entry) => {
5650
+ if (confirmId !== entry.id) {
5651
+ setConfirmId(entry.id);
5652
+ return;
5653
+ }
5654
+ setConfirmId(null);
5655
+ deleteProfile(entry.id);
5656
+ };
5657
+ const profileOptions = (value, onChange) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
5658
+ className: "dab-select",
5659
+ value: value ?? "",
5660
+ onChange: (e) => onChange(e.target.value || null),
5661
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5662
+ value: "",
5663
+ children: t("profileNone")
5664
+ }), profiles.map((pr) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5665
+ value: pr.id,
5666
+ children: pr.name
5667
+ }, pr.id))]
5668
+ });
5669
+ const patchSchedule = (patch) => setSchedule(patch);
4779
5670
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4780
5671
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
4781
5672
  className: "dab-head dab-rise",
@@ -4795,11 +5686,258 @@ window.__ModuleLoader__.load({
4795
5686
  })
4796
5687
  ]
4797
5688
  }),
5689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5690
+ className: "dab-card dab-rise",
5691
+ style: { "--d": 1 },
5692
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5693
+ className: "dab-swatch-title",
5694
+ children: t("presetGalleryTitle")
5695
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5696
+ className: "dab-preset-grid",
5697
+ children: BUILTIN_PRESETS.map((ps) => {
5698
+ const hex = hexOf(ps.appearance.color);
5699
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5700
+ type: "button",
5701
+ className: "dab-preset",
5702
+ onClick: () => {
5703
+ applyPreset(ps.appearance);
5704
+ notify(t("presetApplied"));
5705
+ },
5706
+ children: [
5707
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5708
+ className: "dab-preset-dot",
5709
+ style: hex ? { background: hex } : void 0,
5710
+ children: hex ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 12 })
5711
+ }),
5712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5713
+ className: "dab-preset-name",
5714
+ children: t(`presetName_${ps.key}`)
5715
+ }),
5716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5717
+ className: "dab-preset-desc",
5718
+ children: t(`presetDesc_${ps.key}`)
5719
+ })
5720
+ ]
5721
+ }, ps.key);
5722
+ })
5723
+ })]
5724
+ }),
5725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5726
+ className: "dab-card dab-rise",
5727
+ style: { "--d": 2 },
5728
+ children: [
5729
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5730
+ className: "dab-row-head",
5731
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5732
+ className: "dab-swatch-title",
5733
+ style: { marginBottom: 0 },
5734
+ children: t("profilesTitle")
5735
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5736
+ type: "button",
5737
+ className: "dab-btn dab-btn-primary",
5738
+ onClick: () => setNameOpen((o) => !o),
5739
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, { size: 14 }), t("profileSave")]
5740
+ })]
5741
+ }),
5742
+ nameOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5743
+ className: "dab-urlrow",
5744
+ style: {
5745
+ marginTop: 12,
5746
+ display: "flex",
5747
+ flexWrap: "wrap",
5748
+ gap: 8,
5749
+ alignItems: "center"
5750
+ },
5751
+ children: [
5752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5753
+ type: "text",
5754
+ className: "dab-urlinput",
5755
+ value: nameVal,
5756
+ placeholder: t("profileNamePlaceholder"),
5757
+ maxLength: 60,
5758
+ onChange: (e) => setNameVal(e.target.value),
5759
+ onKeyDown: (e) => {
5760
+ if (e.key === "Enter") {
5761
+ e.preventDefault();
5762
+ onSave();
5763
+ }
5764
+ },
5765
+ autoFocus: true
5766
+ }),
5767
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5768
+ type: "button",
5769
+ className: "dab-btn dab-btn-primary",
5770
+ disabled: !nameVal.trim(),
5771
+ onClick: onSave,
5772
+ children: t("editorCommit")
5773
+ }),
5774
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5775
+ type: "button",
5776
+ className: "dab-btn",
5777
+ onClick: () => {
5778
+ setNameOpen(false);
5779
+ setNameVal("");
5780
+ },
5781
+ children: t("bgUrlCancel")
5782
+ })
5783
+ ]
5784
+ }) : null,
5785
+ profiles.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5786
+ className: "dab-hint",
5787
+ style: { marginTop: 10 },
5788
+ children: t("profilesEmpty")
5789
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5790
+ className: "dab-profile-list",
5791
+ children: profiles.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5792
+ className: `dab-profile-row${activeProfile === entry.id ? " is-active" : ""}`,
5793
+ children: [
5794
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfileDot, { entry }),
5795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5796
+ className: "dab-profile-meta",
5797
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5798
+ className: "dab-profile-name",
5799
+ children: entry.name
5800
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5801
+ className: "dab-profile-sub",
5802
+ children: new Date(entry.createdAt).toLocaleDateString()
5803
+ })]
5804
+ }),
5805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5806
+ type: "button",
5807
+ className: "dab-btn",
5808
+ onClick: () => onApply(entry),
5809
+ children: t("profileApply")
5810
+ }),
5811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5812
+ type: "button",
5813
+ className: `dab-btn dab-btn-ghost dab-btn-danger${confirmId === entry.id ? " dab-btn-confirm" : ""}`,
5814
+ title: t("profileDelete"),
5815
+ onClick: () => onDelete(entry),
5816
+ children: confirmId === entry.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 13 }), t("profileDeleteConfirm")] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 13 })
5817
+ })
5818
+ ]
5819
+ }, entry.id))
5820
+ })
5821
+ ]
5822
+ }),
5823
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5824
+ className: "dab-card dab-rise",
5825
+ style: { "--d": 3 },
5826
+ children: [
5827
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5828
+ className: "dab-row-head",
5829
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5830
+ className: "dab-swatch-title",
5831
+ style: { marginBottom: 0 },
5832
+ children: t("scheduleTitle")
5833
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5834
+ type: "button",
5835
+ className: `dab-toggle${schedule.enabled ? " is-on" : ""}`,
5836
+ role: "switch",
5837
+ "aria-checked": schedule.enabled,
5838
+ onClick: () => patchSchedule({ enabled: !schedule.enabled }),
5839
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
5840
+ })]
5841
+ }),
5842
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5843
+ className: "dab-hint",
5844
+ style: { marginTop: 8 },
5845
+ children: t("scheduleHint")
5846
+ }),
5847
+ schedule.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5848
+ className: "dab-schedule-grid",
5849
+ children: [
5850
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5851
+ className: "dab-schedule-cell",
5852
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5853
+ className: "dab-swatch-title",
5854
+ children: t("scheduleMode")
5855
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5856
+ className: "dab-chip-row",
5857
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5858
+ type: "button",
5859
+ className: `dab-chip${schedule.mode === "time" ? " is-active" : ""}`,
5860
+ onClick: () => patchSchedule({ mode: "time" }),
5861
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClockIcon, { size: 13 }), t("scheduleModeTime")]
5862
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5863
+ type: "button",
5864
+ className: `dab-chip${schedule.mode === "system" ? " is-active" : ""}`,
5865
+ onClick: () => patchSchedule({ mode: "system" }),
5866
+ children: t("scheduleModeSystem")
5867
+ })]
5868
+ })]
5869
+ }),
5870
+ schedule.mode === "time" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5871
+ className: "dab-schedule-cell",
5872
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5873
+ className: "dab-swatch-title",
5874
+ children: t("scheduleTimes")
5875
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5876
+ className: "dab-time-row",
5877
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5878
+ className: "dab-time-label",
5879
+ children: [
5880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }),
5881
+ t("scheduleDayStart"),
5882
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5883
+ type: "time",
5884
+ className: "dab-timeinput",
5885
+ value: schedule.dayStart,
5886
+ onChange: (e) => {
5887
+ if (/^([01]\d|2[0-3]):[0-5]\d$/.test(e.target.value)) patchSchedule({ dayStart: e.target.value });
5888
+ }
5889
+ })
5890
+ ]
5891
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5892
+ className: "dab-time-label",
5893
+ children: [
5894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MoonIcon, { size: 13 }),
5895
+ t("scheduleNightStart"),
5896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5897
+ type: "time",
5898
+ className: "dab-timeinput",
5899
+ value: schedule.nightStart,
5900
+ onChange: (e) => {
5901
+ if (/^([01]\d|2[0-3]):[0-5]\d$/.test(e.target.value)) patchSchedule({ nightStart: e.target.value });
5902
+ }
5903
+ })
5904
+ ]
5905
+ })]
5906
+ })]
5907
+ }) : null,
5908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5909
+ className: "dab-schedule-cell",
5910
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5911
+ className: "dab-swatch-title",
5912
+ children: t("scheduleProfiles")
5913
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5914
+ className: "dab-time-row",
5915
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5916
+ className: "dab-time-label",
5917
+ children: [
5918
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SunIcon, { size: 13 }),
5919
+ t("scheduleDayProfile"),
5920
+ profileOptions(schedule.dayProfile, (id) => patchSchedule({ dayProfile: id }))
5921
+ ]
5922
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5923
+ className: "dab-time-label",
5924
+ children: [
5925
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MoonIcon, { size: 13 }),
5926
+ t("scheduleNightProfile"),
5927
+ profileOptions(schedule.nightProfile, (id) => patchSchedule({ nightProfile: id }))
5928
+ ]
5929
+ })]
5930
+ })]
5931
+ })
5932
+ ]
5933
+ }) : null
5934
+ ]
5935
+ }),
4798
5936
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4799
5937
  className: "dab-profile-grid",
4800
5938
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4801
5939
  className: "dab-card dab-card-hover dab-rise",
4802
- style: { "--d": 1 },
5940
+ style: { "--d": 4 },
4803
5941
  children: [
4804
5942
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4805
5943
  className: "dab-profile-ico",
@@ -4825,7 +5963,7 @@ window.__ModuleLoader__.load({
4825
5963
  ]
4826
5964
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4827
5965
  className: "dab-card dab-card-hover dab-rise",
4828
- style: { "--d": 2 },
5966
+ style: { "--d": 5 },
4829
5967
  children: [
4830
5968
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4831
5969
  className: "dab-profile-ico",
@@ -4862,7 +6000,7 @@ window.__ModuleLoader__.load({
4862
6000
  }),
4863
6001
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
4864
6002
  className: "dab-footer dab-rise",
4865
- style: { "--d": 3 },
6003
+ style: { "--d": 6 },
4866
6004
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4867
6005
  className: "dab-footer-mono",
4868
6006
  children: "dsh-any-background"
@@ -4996,7 +6134,30 @@ window.__ModuleLoader__.load({
4996
6134
  const registerCustom = (h, s, l) => {
4997
6135
  customDispose?.();
4998
6136
  try {
4999
- const { colorScheme, tokens } = genTokens(h, s, l);
6137
+ let colorScheme;
6138
+ let tokens;
6139
+ if (rHasColor()) ({colorScheme, tokens} = genTokens(h ?? rColor()[0], s ?? rColor()[1], l ?? rColor()[2], rColorScheme()));
6140
+ else if (rSchemeOverride() !== "auto") {
6141
+ const dark = rScheme() === "dark";
6142
+ ({colorScheme, tokens} = genTokens(220, .04, dark ? .14 : .92, dark ? "dark" : "light"));
6143
+ } else {
6144
+ const verdict = rBgDark();
6145
+ if (verdict === null) {
6146
+ customDispose = null;
6147
+ return false;
6148
+ }
6149
+ const snap = ctx.theme.getTheme();
6150
+ const wantScheme = verdict ? "dark" : "light";
6151
+ const source = snap.themes.find((t) => t.id !== CUSTOM_ID && t.colorScheme === wantScheme) ?? snap.themes.find((t) => t.id !== CUSTOM_ID);
6152
+ if (source === void 0) {
6153
+ customDispose = null;
6154
+ return false;
6155
+ }
6156
+ colorScheme = wantScheme;
6157
+ tokens = { ...source.tokens };
6158
+ const font = verdict ? "#fff" : "#000";
6159
+ for (const name of LABEL_TOKENS) tokens[name] = font;
6160
+ }
5000
6161
  customDispose = ctx.theme.register({
5001
6162
  id: CUSTOM_ID,
5002
6163
  colorScheme,
@@ -5005,9 +6166,20 @@ window.__ModuleLoader__.load({
5005
6166
  } catch {
5006
6167
  customDispose = null;
5007
6168
  }
5008
- if (ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
6169
+ const present = ctx.theme.getTheme().themes.some((t) => t.id === CUSTOM_ID);
6170
+ if (present) ctx.theme.setTheme(CUSTOM_ID);
6171
+ return present;
5009
6172
  };
5010
6173
  if (rHasColor()) registerCustom(initH, initS, initL);
6174
+ onVerdictApplied(() => {
6175
+ if (!rHasColor()) registerCustom();
6176
+ });
6177
+ onColorAdopted((hsl) => {
6178
+ registerCustom(hsl[0], hsl[1], hsl[2]);
6179
+ saveConfig();
6180
+ colorRev++;
6181
+ bound?.syncColor(hslToHsv(hsl[0], hsl[1], hsl[2]), colorRev);
6182
+ });
5011
6183
  ctx.effect(() => () => {
5012
6184
  customDispose?.();
5013
6185
  if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
@@ -5034,7 +6206,16 @@ window.__ModuleLoader__.load({
5034
6206
  backgroundType: cfg.backgroundType,
5035
6207
  generatedBg: cfg.generatedBg,
5036
6208
  bgRev: -1,
5037
- regenerateOnReload: cfg.regenerateOnReload
6209
+ regenerateOnReload: cfg.regenerateOnReload,
6210
+ profiles: [],
6211
+ rotation: {
6212
+ ...DEFAULT_CONFIG.rotation,
6213
+ items: []
6214
+ },
6215
+ schedule: { ...DEFAULT_CONFIG.schedule },
6216
+ schemeOverride: "auto",
6217
+ activeProfile: null,
6218
+ metaRev: -1
5038
6219
  }),
5039
6220
  actions: {
5040
6221
  syncBg: (d, url, r, bgType, genBg, bgr, reload) => {
@@ -5054,6 +6235,16 @@ window.__ModuleLoader__.load({
5054
6235
  d.color = hsv;
5055
6236
  d.colorRev = r;
5056
6237
  }
6238
+ },
6239
+ syncMeta: (d, profiles, rotation, schedule, schemeOverride, activeProfile, r) => {
6240
+ if (r > d.metaRev) {
6241
+ d.profiles = profiles;
6242
+ d.rotation = rotation;
6243
+ d.schedule = schedule;
6244
+ d.schemeOverride = schemeOverride;
6245
+ d.activeProfile = activeProfile;
6246
+ d.metaRev = r;
6247
+ }
5057
6248
  }
5058
6249
  }
5059
6250
  });
@@ -5064,14 +6255,142 @@ window.__ModuleLoader__.load({
5064
6255
  bound?.syncBg(rWp(), rev, cfg.backgroundType, cfg.generatedBg, bgRev, cfg.regenerateOnReload);
5065
6256
  };
5066
6257
  onGeneratedSnapshot(syncBg);
6258
+ let metaRev = 0;
6259
+ const syncMetaNow = () => {
6260
+ metaRev++;
6261
+ bound?.syncMeta(rProfiles(), rRotation(), rSchedule(), rSchemeOverride(), cfg.activeProfile, metaRev);
6262
+ };
6263
+ /** Apply an appearance snapshot (profile or built-in preset) to the whole
6264
+ * interface: re-register the skin, re-emit tokens, persist. */
6265
+ const applyAppearanceLive = (ap) => {
6266
+ applyAppearance(ap);
6267
+ if (rHasColor()) {
6268
+ const [h, s, l] = rColor();
6269
+ registerCustom(h, s, l);
6270
+ } else if (!registerCustom()) {
6271
+ customDispose?.();
6272
+ customDispose = null;
6273
+ clearThemeTokens();
6274
+ const snap = ctx.theme.getTheme();
6275
+ const fallback = snap.themes.find((t) => t.id !== CUSTOM_ID);
6276
+ if (fallback !== void 0 && snap.preference === CUSTOM_ID) ctx.theme.setTheme(fallback.id);
6277
+ }
6278
+ applyWp();
6279
+ };
6280
+ const applyProfileById = (id) => {
6281
+ const entry = rProfiles().find((p) => p.id === id);
6282
+ if (entry === void 0) return false;
6283
+ applyAppearanceLive(entry.config);
6284
+ cfg.activeProfile = id;
6285
+ applyWp();
6286
+ persistConfig();
6287
+ syncMetaNow();
6288
+ return true;
6289
+ };
6290
+ /** Extract the theme color once from a freshly activated wallpaper and run
6291
+ * the full adaptation (host skin + editor wheel sync); the caller persists. */
6292
+ const adoptWallpaperColor = async (dataUrl) => {
6293
+ const hsl = await extractWallpaperColor(dataUrl, rBgState());
6294
+ if (!hsl) return;
6295
+ cfg.color = hsl;
6296
+ registerCustom(hsl[0], hsl[1], hsl[2]);
6297
+ colorRev++;
6298
+ bound?.syncColor(hslToHsv(hsl[0], hsl[1], hsl[2]), colorRev);
6299
+ };
6300
+ /** Activate a rotation item: the server copies its bytes into the wallpaper
6301
+ * slot; the client applies the returned data URL through the normal image
6302
+ * path, then re-extracts the theme color from the new picture so the
6303
+ * palette follows the rotation. */
6304
+ const applyRotationIndex = async (idx, auto) => {
6305
+ const rot = rRotation();
6306
+ if (idx < 0 || idx >= rot.items.length) return false;
6307
+ const r = await rotationActivate(idx);
6308
+ if (!r.ok || !r.dataUrl) {
6309
+ console.warn("dsh-any-background: rotation activate failed", r.error);
6310
+ return false;
6311
+ }
6312
+ setWpImageUrl(r.dataUrl);
6313
+ if (cfg.backgroundType === "image") setWpUrl(r.dataUrl);
6314
+ cfg.rotation = {
6315
+ ...rot,
6316
+ current: idx,
6317
+ lastRotate: auto || rot.lastRotate === null ? (/* @__PURE__ */ new Date()).toISOString() : rot.lastRotate
6318
+ };
6319
+ if (cfg.backgroundType === "image") await adoptWallpaperColor(r.dataUrl);
6320
+ applyThemeColor();
6321
+ syncBg();
6322
+ saveConfig();
6323
+ syncMetaNow();
6324
+ return true;
6325
+ };
6326
+ const isoWeekKey = (d) => {
6327
+ const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
6328
+ const day = t.getUTCDay() || 7;
6329
+ t.setUTCDate(t.getUTCDate() + 4 - day);
6330
+ const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
6331
+ const week = Math.ceil(((t.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
6332
+ return `${t.getUTCFullYear()}-W${week}`;
6333
+ };
6334
+ const rotationDue = () => {
6335
+ const rot = rRotation();
6336
+ if (!rot.enabled || rot.items.length === 0) return false;
6337
+ if (rot.interval === "reload") return true;
6338
+ const last = rot.lastRotate !== null ? new Date(rot.lastRotate) : null;
6339
+ if (last === null || isNaN(last.getTime())) return true;
6340
+ const now = /* @__PURE__ */ new Date();
6341
+ if (rot.interval === "daily") return last.toDateString() !== now.toDateString();
6342
+ return isoWeekKey(last) !== isoWeekKey(now);
6343
+ };
6344
+ const pickNextRotationIndex = () => {
6345
+ const rot = rRotation();
6346
+ const n = rot.items.length;
6347
+ if (n === 0) return -1;
6348
+ if (rot.mode === "shuffle" && n > 1) {
6349
+ let idx = rot.current;
6350
+ while (idx === rot.current) idx = Math.floor(Math.random() * n);
6351
+ return idx;
6352
+ }
6353
+ return (rot.current + 1) % n;
6354
+ };
6355
+ const maybeRotate = async () => {
6356
+ if (!rotationDue()) return;
6357
+ await applyRotationIndex(pickNextRotationIndex(), true);
6358
+ };
6359
+ const parseHHMM = (s) => {
6360
+ const m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(s);
6361
+ return m === null ? -1 : Number(m[1]) * 60 + Number(m[2]);
6362
+ };
6363
+ const isNightNow = (sc) => {
6364
+ const now = /* @__PURE__ */ new Date();
6365
+ const cur = now.getHours() * 60 + now.getMinutes();
6366
+ const day = parseHHMM(sc.dayStart);
6367
+ const night = parseHHMM(sc.nightStart);
6368
+ if (day < 0 || night < 0) return false;
6369
+ if (day <= night) return cur >= night || cur < day;
6370
+ return cur >= night && cur < day;
6371
+ };
6372
+ const scheduleTick = () => {
6373
+ const sc = rSchedule();
6374
+ if (!sc.enabled) return;
6375
+ const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false : isNightNow(sc)) ? sc.nightProfile : sc.dayProfile;
6376
+ if (!want || want === cfg.activeProfile) return;
6377
+ applyProfileById(want);
6378
+ };
5067
6379
  applyWp();
5068
6380
  syncBg();
5069
6381
  watchParts();
5070
- loadPersisted().then(() => {
6382
+ loadPersisted().then(async (serverRotated) => {
6383
+ syncMetaNow();
5071
6384
  if (rHasColor()) {
5072
6385
  const [h, s, l] = rColor();
5073
6386
  registerCustom(h, s, l);
5074
6387
  }
6388
+ if (serverRotated) {
6389
+ if (cfg.backgroundType === "image" && rWp()) {
6390
+ await adoptWallpaperColor(rWp());
6391
+ saveConfig();
6392
+ }
6393
+ } else await maybeRotate();
5075
6394
  if (cfg.backgroundType === "video") {
5076
6395
  const v = rWpVideo();
5077
6396
  if (v) {
@@ -5093,16 +6412,24 @@ window.__ModuleLoader__.load({
5093
6412
  persistConfig();
5094
6413
  } else applyThemeColor();
5095
6414
  syncBg();
6415
+ scheduleTick();
5096
6416
  if (rHasColor()) {
5097
6417
  colorRev++;
5098
6418
  bound?.syncColor(hslToHsv(...rColor()), colorRev);
5099
6419
  }
5100
6420
  });
6421
+ const schemeMq = window.matchMedia?.("(prefers-color-scheme: dark)");
6422
+ const scheduleTimer = window.setInterval(scheduleTick, 3e4);
6423
+ schemeMq?.addEventListener?.("change", scheduleTick);
6424
+ ctx.effect(() => () => {
6425
+ window.clearInterval(scheduleTimer);
6426
+ schemeMq?.removeEventListener?.("change", scheduleTick);
6427
+ }, "dsh-any-background: schedule timer");
5101
6428
  ctx.effect(() => () => {
5102
6429
  teardownWp();
5103
6430
  }, "dsh-any-background: wp cleanup");
5104
6431
  ctx.effect(() => ctx.on("theme/change", () => {
5105
- if (rHasColor()) {
6432
+ if (rHasColor() || rBgDark() !== null || rSchemeOverride() !== "auto") {
5106
6433
  const snapshot = ctx.theme.getTheme();
5107
6434
  if (snapshot.preference !== CUSTOM_ID && snapshot.themes.some((t) => t.id === CUSTOM_ID)) ctx.theme.setTheme(CUSTOM_ID);
5108
6435
  }
@@ -5135,6 +6462,7 @@ window.__ModuleLoader__.load({
5135
6462
  const sectionInject = (actions) => {
5136
6463
  bound = actions;
5137
6464
  syncBg();
6465
+ syncMetaNow();
5138
6466
  const playVideoFromBlob = (blob, mime) => {
5139
6467
  const localUrl = URL.createObjectURL(blob);
5140
6468
  cfg.backgroundType = "video";
@@ -5388,6 +6716,111 @@ window.__ModuleLoader__.load({
5388
6716
  } catch {
5389
6717
  return false;
5390
6718
  }
6719
+ },
6720
+ saveProfile: (name) => {
6721
+ const trimmed = name.trim();
6722
+ if (!trimmed) return false;
6723
+ const profiles = [...rProfiles()];
6724
+ if (profiles.length >= 20) profiles.shift();
6725
+ const id = `p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
6726
+ profiles.push({
6727
+ id,
6728
+ name: trimmed.slice(0, 60),
6729
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6730
+ config: currentAppearance()
6731
+ });
6732
+ cfg.profiles = profiles;
6733
+ cfg.activeProfile = id;
6734
+ persistConfig();
6735
+ syncMetaNow();
6736
+ return true;
6737
+ },
6738
+ applyProfile: (id) => applyProfileById(id),
6739
+ deleteProfile: (id) => {
6740
+ const before = rProfiles();
6741
+ if (!before.some((p) => p.id === id)) return false;
6742
+ cfg.profiles = before.filter((p) => p.id !== id);
6743
+ if (cfg.activeProfile === id) cfg.activeProfile = null;
6744
+ const sc = rSchedule();
6745
+ if (sc.dayProfile === id || sc.nightProfile === id) cfg.schedule = {
6746
+ ...sc,
6747
+ dayProfile: sc.dayProfile === id ? null : sc.dayProfile,
6748
+ nightProfile: sc.nightProfile === id ? null : sc.nightProfile
6749
+ };
6750
+ persistConfig();
6751
+ syncMetaNow();
6752
+ return true;
6753
+ },
6754
+ applyPreset: (appearance) => {
6755
+ applyAppearanceLive(appearance);
6756
+ cfg.activeProfile = null;
6757
+ applyWp();
6758
+ persistConfig();
6759
+ syncMetaNow();
6760
+ },
6761
+ setSchemeOverride: (v) => {
6762
+ cfg.schemeOverride = v;
6763
+ registerCustom();
6764
+ applyWp();
6765
+ saveConfig();
6766
+ syncMetaNow();
6767
+ },
6768
+ setSchedule: (patch) => {
6769
+ cfg.schedule = {
6770
+ ...rSchedule(),
6771
+ ...patch
6772
+ };
6773
+ if (patch.enabled === true) scheduleTick();
6774
+ persistConfig();
6775
+ syncMetaNow();
6776
+ },
6777
+ setRotation: (patch) => {
6778
+ cfg.rotation = {
6779
+ ...rRotation(),
6780
+ ...patch
6781
+ };
6782
+ if (patch.enabled === true) maybeRotate();
6783
+ persistConfig();
6784
+ syncMetaNow();
6785
+ },
6786
+ addRotationItems: async (files) => {
6787
+ let added = false;
6788
+ for (const f of files) {
6789
+ if (!f.type.startsWith("image/")) continue;
6790
+ const dataUrl = await readImgAsync(f);
6791
+ if (!dataUrl) continue;
6792
+ const r = await rotationAdd(dataUrl, await makeThumb(dataUrl) ?? "");
6793
+ if (r.ok && r.items !== void 0) {
6794
+ cfg.rotation = {
6795
+ ...rRotation(),
6796
+ items: r.items
6797
+ };
6798
+ added = true;
6799
+ }
6800
+ }
6801
+ if (added) {
6802
+ persistConfig();
6803
+ syncMetaNow();
6804
+ syncBg();
6805
+ }
6806
+ return added;
6807
+ },
6808
+ removeRotationItem: async (index) => {
6809
+ const r = await rotationRemove(index);
6810
+ if (!r.ok || r.items === void 0) return false;
6811
+ const rot = rRotation();
6812
+ cfg.rotation = {
6813
+ ...rot,
6814
+ items: r.items,
6815
+ current: Math.max(0, Math.min(rot.current >= index ? rot.current - 1 : rot.current, Math.max(0, r.items.length - 1)))
6816
+ };
6817
+ persistConfig();
6818
+ syncMetaNow();
6819
+ return true;
6820
+ },
6821
+ rotateNow: async () => {
6822
+ if (rRotation().items.length === 0) return false;
6823
+ return applyRotationIndex(pickNextRotationIndex(), true);
5391
6824
  }
5392
6825
  };
5393
6826
  };
@@ -5463,12 +6896,11 @@ window.__ModuleLoader__.load({
5463
6896
  restoreTimers.forEach((id) => window.clearTimeout(id));
5464
6897
  }, "dsh-any-background: boot restore");
5465
6898
  const watchdogId = window.setInterval(() => {
5466
- if (!rHasColor()) return;
6899
+ if (!rHasColor() && rBgDark() === null && rSchemeOverride() === "auto") return;
5467
6900
  const snapshot = ctx.theme.getTheme();
5468
6901
  let changed = false;
5469
6902
  if (!snapshot.themes.some((t) => t.id === CUSTOM_ID)) {
5470
- const [h, s, l] = rColor();
5471
- registerCustom(h, s, l);
6903
+ registerCustom();
5472
6904
  changed = true;
5473
6905
  } else if (snapshot.preference !== CUSTOM_ID) {
5474
6906
  ctx.theme.setTheme(CUSTOM_ID);