dsh-any-background 0.3.10 → 0.4.1

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
@@ -27,7 +27,6 @@ window.__ModuleLoader__.load({
27
27
  const zh = {
28
28
  nav: "主题",
29
29
  brandTag: "外观定制",
30
- close: "关闭",
31
30
  pageColor: "色彩",
32
31
  pageInterface: "界面",
33
32
  pageFont: "字体",
@@ -78,7 +77,6 @@ window.__ModuleLoader__.load({
78
77
  strokeTitle: "文字描边",
79
78
  strokeHint: "按区域为文字添加描边:粗细为 0 表示关闭,「自动」会生成与文字颜色相反的颜色,「主题色」跟随当前主题色。代码块与图标已自动豁免,多色语法不会糊成一团",
80
79
  strokeWidth: "描边粗细",
81
- strokeColor: "描边颜色",
82
80
  strokeAuto: "自动反色",
83
81
  strokeGray: "灰色",
84
82
  strokeBlack: "黑色",
@@ -118,7 +116,6 @@ window.__ModuleLoader__.load({
118
116
  editorReset: "重置",
119
117
  extractColor: "从背景提取主题色",
120
118
  extracting: "取色中…",
121
- extractNoWp: "请先设置背景图片",
122
119
  extractDone: "已应用背景主色调",
123
120
  extractFail: "未在背景中找到鲜明的颜色,请换一张背景",
124
121
  crashTitle: "界面渲染出错",
@@ -138,7 +135,6 @@ window.__ModuleLoader__.load({
138
135
  pickerTitle: "从背景取色",
139
136
  pickerHint: "移动鼠标预览颜色,点击背景选取为主题色",
140
137
  pickerClose: "关闭",
141
- bgTypeImage: "图片",
142
138
  bgTypeMesh: "网格渐变",
143
139
  bgTypeShader: "Shader",
144
140
  bgTypePattern: "几何图案",
@@ -194,8 +190,12 @@ window.__ModuleLoader__.load({
194
190
  profileNamePlaceholder: "输入配置名称…",
195
191
  profileNameRequired: "请先输入配置名称",
196
192
  profileSaved: "配置已保存",
193
+ profileRenamed: "配置已重命名",
194
+ profileDuplicated: "配置已复制",
197
195
  profileApplied: "配置已应用",
198
196
  profileApply: "应用",
197
+ profileRename: "重命名",
198
+ profileDuplicate: "复制配置",
199
199
  profileDelete: "删除配置",
200
200
  profileDeleteConfirm: "确认删除",
201
201
  profileNone: "不切换",
@@ -218,6 +218,7 @@ window.__ModuleLoader__.load({
218
218
  rotShuffle: "随机",
219
219
  rotOrder: "顺序",
220
220
  rotReload: "每次刷新",
221
+ rotHourly: "每小时",
221
222
  rotDaily: "每天",
222
223
  rotWeekly: "每周",
223
224
  rotNow: "立即切换"
@@ -225,7 +226,6 @@ window.__ModuleLoader__.load({
225
226
  const en = {
226
227
  nav: "Theme",
227
228
  brandTag: "Appearance",
228
- close: "Close",
229
229
  pageColor: "Color",
230
230
  pageInterface: "Interface",
231
231
  pageFont: "Font",
@@ -256,7 +256,7 @@ window.__ModuleLoader__.load({
256
256
  uiHeader: "Header popovers",
257
257
  uiHeaderBar: "Header bar",
258
258
  captionInkLabel: "Caption strip text",
259
- captionInkHint: "Who paints the text on the desktop caption strip — the native 「应用 / 编辑」 menu bar and the sidebar toggle. The strip is the sidebar material over the wallpaper, so it can end up on the opposite side of the theme.",
259
+ captionInkHint: "Who paints the text on the desktop caption strip — the native \"App / Edit\" menu bar and the sidebar toggle. The strip is the sidebar material over the wallpaper, so it can end up on the opposite side of the theme.",
260
260
  captionInkAuto: "Auto",
261
261
  captionInkTheme: "Follow theme",
262
262
  captionInkWhite: "Always white",
@@ -276,7 +276,6 @@ window.__ModuleLoader__.load({
276
276
  strokeTitle: "Text outline",
277
277
  strokeHint: "Outline text per region: width 0 turns it off, \"auto\" picks the inverse of the text color, and \"theme\" follows the current accent. Code blocks and icons are exempted automatically, so multi-color syntax never smears",
278
278
  strokeWidth: "Outline width",
279
- strokeColor: "Outline color",
280
279
  strokeAuto: "Auto contrast",
281
280
  strokeGray: "Gray",
282
281
  strokeBlack: "Black",
@@ -316,7 +315,6 @@ window.__ModuleLoader__.load({
316
315
  editorReset: "Reset",
317
316
  extractColor: "Extract from background",
318
317
  extracting: "Extracting…",
319
- extractNoWp: "Set a background first",
320
318
  extractDone: "Background color applied",
321
319
  extractFail: "No vivid color found in this background, try another",
322
320
  crashTitle: "Section crashed",
@@ -336,7 +334,6 @@ window.__ModuleLoader__.load({
336
334
  pickerTitle: "Pick from background",
337
335
  pickerHint: "Hover to preview, click to pick as theme color",
338
336
  pickerClose: "Close",
339
- bgTypeImage: "Image",
340
337
  bgTypeMesh: "Mesh gradient",
341
338
  bgTypeShader: "Shader",
342
339
  bgTypePattern: "Pattern",
@@ -392,8 +389,12 @@ window.__ModuleLoader__.load({
392
389
  profileNamePlaceholder: "Profile name…",
393
390
  profileNameRequired: "Enter a name first",
394
391
  profileSaved: "Profile saved",
392
+ profileRenamed: "Profile renamed",
393
+ profileDuplicated: "Profile duplicated",
395
394
  profileApplied: "Profile applied",
396
395
  profileApply: "Apply",
396
+ profileRename: "Rename",
397
+ profileDuplicate: "Duplicate",
397
398
  profileDelete: "Delete profile",
398
399
  profileDeleteConfirm: "Sure?",
399
400
  profileNone: "None",
@@ -416,6 +417,7 @@ window.__ModuleLoader__.load({
416
417
  rotShuffle: "Shuffle",
417
418
  rotOrder: "In order",
418
419
  rotReload: "Every refresh",
420
+ rotHourly: "Hourly",
419
421
  rotDaily: "Daily",
420
422
  rotWeekly: "Weekly",
421
423
  rotNow: "Switch now"
@@ -902,7 +904,7 @@ window.__ModuleLoader__.load({
902
904
  return {
903
905
  enabled: r.enabled === true,
904
906
  mode: r.mode === "order" ? "order" : "shuffle",
905
- interval: r.interval === "reload" || r.interval === "weekly" ? r.interval : "daily",
907
+ interval: r.interval === "reload" || r.interval === "hourly" || r.interval === "weekly" ? r.interval : "daily",
906
908
  current: num(r.current, 0),
907
909
  items,
908
910
  lastRotate: typeof r.lastRotate === "string" ? r.lastRotate : null
@@ -1879,22 +1881,44 @@ window.__ModuleLoader__.load({
1879
1881
  }
1880
1882
  const SAVE_DEBOUNCE_MS = 250;
1881
1883
  let saveTimer;
1884
+ /** Write the config and SURFACE a failed persist. The server answers
1885
+ * `ok:true` with `value:false` when the DISK write failed — the transport
1886
+ * succeeded, so `rpcCall` returns the boolean without a word and every caller
1887
+ * used to assume the save had landed. Retry once after a beat (the common
1888
+ * Windows cause is a transient EBUSY from an antivirus scan holding the
1889
+ * file), then say it in the log so a "settings reverted" report has a trail. */
1890
+ let writeRetryQueued = false;
1891
+ async function writeNow() {
1892
+ if (await rpcCall("writeConfig", { config: cfg }) === true) {
1893
+ writeRetryQueued = false;
1894
+ return;
1895
+ }
1896
+ if (!writeRetryQueued) {
1897
+ writeRetryQueued = true;
1898
+ window.setTimeout(() => {
1899
+ writeNow();
1900
+ }, 1e3);
1901
+ return;
1902
+ }
1903
+ writeRetryQueued = false;
1904
+ console.warn("dsh-any-background: the host could not persist the config — recent settings may be lost on reload (see the host log for the write error)");
1905
+ }
1882
1906
  function saveConfig() {
1883
1907
  if (saveTimer !== void 0) window.clearTimeout(saveTimer);
1884
1908
  saveTimer = window.setTimeout(() => {
1885
1909
  saveTimer = void 0;
1886
- rpcCall("writeConfig", { config: cfg });
1910
+ writeNow();
1887
1911
  }, SAVE_DEBOUNCE_MS);
1888
1912
  }
1889
1913
  function flushSave() {
1890
1914
  if (saveTimer === void 0) return;
1891
1915
  window.clearTimeout(saveTimer);
1892
1916
  saveTimer = void 0;
1893
- rpcCall("writeConfig", { config: cfg });
1917
+ writeNow();
1894
1918
  }
1895
1919
  /** Persist the current config immediately (import path — no debounce). */
1896
1920
  function persistConfig() {
1897
- rpcCall("writeConfig", { config: cfg });
1921
+ writeNow();
1898
1922
  }
1899
1923
  /** Load the persisted theme (config + wallpaper URL + video URL) from the node
1900
1924
  * half. Image and video both travel as serve URLs — never bytes — so this RPC
@@ -2092,6 +2116,10 @@ window.__ModuleLoader__.load({
2092
2116
  async function removeFont() {
2093
2117
  return await rpcCall("removeFont", {}) === true;
2094
2118
  }
2119
+ /** Serve URL of one rotation-pool image (the export path fetches its bytes). */
2120
+ function rotationFileUrl(file) {
2121
+ return `/dsh-any-background/rotation/file?f=${encodeURIComponent(file)}`;
2122
+ }
2095
2123
  /** Add an image (data URL + small thumbnail) to the server-side rotation pool. */
2096
2124
  async function rotationAdd(dataUrl, thumb) {
2097
2125
  if (!rpcCallFn) return {
@@ -2165,6 +2193,32 @@ window.__ModuleLoader__.load({
2165
2193
  };
2166
2194
  }
2167
2195
  }
2196
+ /** Replace the whole server-side rotation pool (theme-import path): the bytes
2197
+ * land under their exported file names, so the imported config's pool
2198
+ * metadata is live on this machine instead of pointing at files it does not
2199
+ * have. */
2200
+ async function rotationRestore(items) {
2201
+ if (!rpcCallFn) return {
2202
+ ok: false,
2203
+ error: "rpc not ready"
2204
+ };
2205
+ try {
2206
+ const res = await rpcCallFn(rpcEndpoint("rotationRestore"), { items });
2207
+ if (res && res.ok === true) return res.value ?? {
2208
+ ok: false,
2209
+ error: "no value"
2210
+ };
2211
+ return {
2212
+ ok: false,
2213
+ error: res?.error?.message ?? "request failed"
2214
+ };
2215
+ } catch (e) {
2216
+ return {
2217
+ ok: false,
2218
+ error: e instanceof Error ? e.message : String(e)
2219
+ };
2220
+ }
2221
+ }
2168
2222
  //#endregion
2169
2223
  //#region src/client/host-compat/capabilities.ts
2170
2224
  /**
@@ -2188,6 +2242,32 @@ window.__ModuleLoader__.load({
2188
2242
  return rHostAdapter();
2189
2243
  }
2190
2244
  //#endregion
2245
+ //#region src/client/dom-watch.ts
2246
+ const listeners = /* @__PURE__ */ new Set();
2247
+ let observer = null;
2248
+ /** Subscribe to body-subtree mutations. Returns an unsubscribe. */
2249
+ function watchDom(cb) {
2250
+ listeners.add(cb);
2251
+ if (observer === null && typeof MutationObserver !== "undefined" && typeof document !== "undefined") {
2252
+ observer = new MutationObserver((records) => {
2253
+ for (const l of listeners) l(records);
2254
+ });
2255
+ observer.observe(document.body, {
2256
+ childList: true,
2257
+ subtree: true,
2258
+ attributes: true,
2259
+ attributeFilter: ["aria-expanded"]
2260
+ });
2261
+ }
2262
+ return () => {
2263
+ listeners.delete(cb);
2264
+ if (listeners.size === 0) {
2265
+ observer?.disconnect();
2266
+ observer = null;
2267
+ }
2268
+ };
2269
+ }
2270
+ //#endregion
2191
2271
  //#region src/client/header-tag.ts
2192
2272
  /**
2193
2273
  * Header-surface tagging.
@@ -2292,7 +2372,6 @@ window.__ModuleLoader__.load({
2292
2372
  if (el.getAttribute("aria-modal") === "true") return false;
2293
2373
  return el.parentElement === document.body || el.closest(headerSlotAnchors()) !== null;
2294
2374
  }
2295
- let observer = null;
2296
2375
  let pending = null;
2297
2376
  /** False once the effect is torn down, so a pass already queued on the microtask queue
2298
2377
  * cannot re-mark a popover after the teardown cleared it. */
@@ -2333,17 +2412,10 @@ window.__ModuleLoader__.load({
2333
2412
  if (typeof MutationObserver === "undefined" || typeof document === "undefined") return () => {};
2334
2413
  active = true;
2335
2414
  refreshHeaderPopovers();
2336
- observer = new MutationObserver(schedule);
2337
- observer.observe(document.body, {
2338
- childList: true,
2339
- subtree: true,
2340
- attributes: true,
2341
- attributeFilter: ["aria-expanded"]
2342
- });
2415
+ const dispose = watchDom(() => schedule());
2343
2416
  return () => {
2344
2417
  active = false;
2345
- observer?.disconnect();
2346
- observer = null;
2418
+ dispose();
2347
2419
  for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
2348
2420
  };
2349
2421
  }
@@ -5176,8 +5248,11 @@ window.__ModuleLoader__.load({
5176
5248
  });
5177
5249
  syncTableFix();
5178
5250
  }
5179
- let partsObserver = null;
5180
- let partsApplyRaf = 0;
5251
+ let partsDispose = null;
5252
+ let partsPending = null;
5253
+ /** False once stopped, so a pass already queued on the microtask queue cannot
5254
+ * re-style anything after the teardown. */
5255
+ let partsActive = false;
5181
5256
  /** Signature of the last blur+opacity pass we actually wrote to the DOM. */
5182
5257
  let appliedPartsKey = "";
5183
5258
  /** The surfaces the last pass styled, compared BY IDENTITY: the host can swap
@@ -5217,11 +5292,13 @@ window.__ModuleLoader__.load({
5217
5292
  /** Watch for the AppFrame mounting so persisted blurs land even when the shell
5218
5293
  * renders after this plugin's apply. */
5219
5294
  function watchParts() {
5220
- if (partsObserver !== null || typeof MutationObserver === "undefined") return;
5221
- partsObserver = new MutationObserver(() => {
5222
- if (partsApplyRaf !== 0) return;
5223
- partsApplyRaf = requestAnimationFrame(() => {
5224
- partsApplyRaf = 0;
5295
+ if (partsDispose !== null || typeof MutationObserver === "undefined") return;
5296
+ partsActive = true;
5297
+ partsDispose = watchDom(() => {
5298
+ if (partsPending !== null || !partsActive) return;
5299
+ partsPending = Promise.resolve().then(() => {
5300
+ partsPending = null;
5301
+ if (!partsActive) return;
5225
5302
  const blurs = rBlurs();
5226
5303
  const ops = rOps();
5227
5304
  const key = JSON.stringify([blurs, ops]);
@@ -5232,18 +5309,12 @@ window.__ModuleLoader__.load({
5232
5309
  appliedTargets = targetsNow();
5233
5310
  });
5234
5311
  });
5235
- partsObserver.observe(document.body, {
5236
- childList: true,
5237
- subtree: true
5238
- });
5239
5312
  }
5240
5313
  function stopWatchingParts() {
5241
- partsObserver?.disconnect();
5242
- partsObserver = null;
5243
- if (partsApplyRaf !== 0) {
5244
- cancelAnimationFrame(partsApplyRaf);
5245
- partsApplyRaf = 0;
5246
- }
5314
+ partsDispose?.();
5315
+ partsDispose = null;
5316
+ partsActive = false;
5317
+ partsPending = null;
5247
5318
  appliedPartsKey = "";
5248
5319
  appliedTargets = [];
5249
5320
  markerSeen.clear();
@@ -5556,8 +5627,6 @@ window.__ModuleLoader__.load({
5556
5627
  applyTrajectoryOverrides(rTrajectoryOpacity());
5557
5628
  }
5558
5629
  applyPanelOverrides(rPanelOpacity());
5559
- applyProduced();
5560
- applyHeaderPopovers();
5561
5630
  applyExemptDefaults();
5562
5631
  applyPartBlurs(rBlurs());
5563
5632
  applyStrokes();
@@ -8793,6 +8862,12 @@ window.__ModuleLoader__.load({
8793
8862
  onClick: () => setRotation({ interval: "reload" }),
8794
8863
  children: t("rotReload")
8795
8864
  }),
8865
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8866
+ type: "button",
8867
+ className: `dab-chip${rotation.interval === "hourly" ? " is-active" : ""}`,
8868
+ onClick: () => setRotation({ interval: "hourly" }),
8869
+ children: t("rotHourly")
8870
+ }),
8796
8871
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8797
8872
  type: "button",
8798
8873
  className: `dab-chip${rotation.interval === "daily" ? " is-active" : ""}`,
@@ -9322,7 +9397,7 @@ window.__ModuleLoader__.load({
9322
9397
  });
9323
9398
  }
9324
9399
  function ProfilePage({ p, notify }) {
9325
- const { t, exportTheme, importTheme, saveProfile, applyProfile, deleteProfile, applyPreset, setSchedule, useStore } = p;
9400
+ const { t, exportTheme, importTheme, saveProfile, applyProfile, deleteProfile, renameProfile, duplicateProfile, applyPreset, setSchedule, useStore } = p;
9326
9401
  const profiles = useStore((s) => s.profiles);
9327
9402
  const schedule = useStore((s) => s.schedule);
9328
9403
  const activeProfile = useStore((s) => s.activeProfile);
@@ -9330,6 +9405,8 @@ window.__ModuleLoader__.load({
9330
9405
  const [nameOpen, setNameOpen] = (0, react.useState)(false);
9331
9406
  const [nameVal, setNameVal] = (0, react.useState)("");
9332
9407
  const [confirmId, setConfirmId] = (0, react.useState)(null);
9408
+ const [editId, setEditId] = (0, react.useState)(null);
9409
+ const [editVal, setEditVal] = (0, react.useState)("");
9333
9410
  const onImport = async (file) => {
9334
9411
  try {
9335
9412
  const ok = await importTheme(file);
@@ -9358,6 +9435,14 @@ window.__ModuleLoader__.load({
9358
9435
  setConfirmId(null);
9359
9436
  deleteProfile(entry.id);
9360
9437
  };
9438
+ const onRenameCommit = (entry) => {
9439
+ const name = editVal.trim();
9440
+ setEditId(null);
9441
+ if (name && name !== entry.name && renameProfile(entry.id, name)) notify(t("profileRenamed"), true);
9442
+ };
9443
+ const onDuplicate = (entry) => {
9444
+ if (duplicateProfile(entry.id)) notify(t("profileDuplicated"), true);
9445
+ };
9361
9446
  const profileOptions = (value, onChange) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
9362
9447
  className: "dab-select",
9363
9448
  value: value ?? "",
@@ -9489,8 +9574,42 @@ window.__ModuleLoader__.load({
9489
9574
  children: profiles.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9490
9575
  className: `dab-profile-row${activeProfile === entry.id ? " is-active" : ""}`,
9491
9576
  style: { "--i": i },
9492
- children: [
9493
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfileDot, { entry }),
9577
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfileDot, { entry }), editId === entry.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9578
+ className: "dab-urlrow",
9579
+ style: {
9580
+ flex: 1,
9581
+ minWidth: 0
9582
+ },
9583
+ children: [
9584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
9585
+ type: "text",
9586
+ className: "dab-urlinput",
9587
+ value: editVal,
9588
+ maxLength: 60,
9589
+ onChange: (e) => setEditVal(e.target.value),
9590
+ onKeyDown: (e) => {
9591
+ if (e.key === "Enter") {
9592
+ e.preventDefault();
9593
+ onRenameCommit(entry);
9594
+ }
9595
+ },
9596
+ autoFocus: true
9597
+ }),
9598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9599
+ type: "button",
9600
+ className: "dab-btn dab-btn-primary",
9601
+ disabled: !editVal.trim(),
9602
+ onClick: () => onRenameCommit(entry),
9603
+ children: t("editorCommit")
9604
+ }),
9605
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9606
+ type: "button",
9607
+ className: "dab-btn",
9608
+ onClick: () => setEditId(null),
9609
+ children: t("bgUrlCancel")
9610
+ })
9611
+ ]
9612
+ }) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9494
9613
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9495
9614
  className: "dab-profile-meta",
9496
9615
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -9507,6 +9626,23 @@ window.__ModuleLoader__.load({
9507
9626
  onClick: () => onApply(entry),
9508
9627
  children: t("profileApply")
9509
9628
  }),
9629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9630
+ type: "button",
9631
+ className: "dab-btn",
9632
+ title: t("profileRename"),
9633
+ onClick: () => {
9634
+ setEditId(entry.id);
9635
+ setEditVal(entry.name);
9636
+ },
9637
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 13 })
9638
+ }),
9639
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9640
+ type: "button",
9641
+ className: "dab-btn",
9642
+ title: t("profileDuplicate"),
9643
+ onClick: () => onDuplicate(entry),
9644
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, { size: 13 })
9645
+ }),
9510
9646
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9511
9647
  type: "button",
9512
9648
  className: `dab-btn dab-btn-danger-solid${confirmId === entry.id ? " dab-btn-confirm" : ""}`,
@@ -9514,7 +9650,7 @@ window.__ModuleLoader__.load({
9514
9650
  onClick: () => onDelete(entry),
9515
9651
  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")
9516
9652
  })
9517
- ]
9653
+ ] })]
9518
9654
  }, entry.id))
9519
9655
  })
9520
9656
  ]
@@ -10048,9 +10184,9 @@ window.__ModuleLoader__.load({
10048
10184
  * unsubscribe for plugin teardown. */
10049
10185
  function startBetterSidebarWatch() {
10050
10186
  sync();
10051
- const observer = new MutationObserver(sync);
10052
- observer.observe(document.body, { childList: true });
10053
- return () => observer.disconnect();
10187
+ return watchDom((records) => {
10188
+ if (records.some((r) => r.target === document.body && r.type === "childList")) sync();
10189
+ });
10054
10190
  }
10055
10191
  let nativeTabs = false;
10056
10192
  const nativeSubs = /* @__PURE__ */ new Set();
@@ -10171,6 +10307,50 @@ window.__ModuleLoader__.load({
10171
10307
  });
10172
10308
  }
10173
10309
  //#endregion
10310
+ //#region src/shared/rotation.ts
10311
+ /** ISO week key (Monday-first). */
10312
+ function isoWeekKey(d) {
10313
+ const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
10314
+ const day = t.getUTCDay() || 7;
10315
+ t.setUTCDate(t.getUTCDate() + 4 - day);
10316
+ const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
10317
+ const week = Math.ceil(((t.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
10318
+ return `${t.getUTCFullYear()}-W${week}`;
10319
+ }
10320
+ /** Whether the pool should advance right now. */
10321
+ function rotationDue(rot, now = /* @__PURE__ */ new Date()) {
10322
+ if (!rot.enabled || rot.items.length === 0) return false;
10323
+ if (rot.interval === "reload") return true;
10324
+ const last = rot.lastRotate !== null ? new Date(rot.lastRotate) : null;
10325
+ if (last === null || isNaN(last.getTime())) return true;
10326
+ if (rot.interval === "hourly") return hourKey(last) !== hourKey(now);
10327
+ if (rot.interval === "daily") return last.toDateString() !== now.toDateString();
10328
+ return isoWeekKey(last) !== isoWeekKey(now);
10329
+ }
10330
+ /** Epoch-hour bucket: hourly cadence advances on each full hour boundary
10331
+ * (in whole-hour timezones — UTC+8 among them — that is exactly the local
10332
+ * on-the-hour tick). */
10333
+ const hourKey = (d) => Math.floor(d.getTime() / 36e5);
10334
+ /** Whether an OPEN page should keep advancing this cadence on its timer.
10335
+ * Only the time-based cadences tick live; 'reload' is a once-per-READ pick
10336
+ * (the server advances it inside the `read` itself), so a live tick driving
10337
+ * it would flip the wallpaper every 30 s — exactly the v0.4.0 regression
10338
+ * (rotationDue is unconditionally true for 'reload'). */
10339
+ function rotationAdvancesLive(rot) {
10340
+ return rot.interval !== "reload";
10341
+ }
10342
+ /** Pick the next pool index under the configured mode. */
10343
+ function pickNextRotationIndex(rot) {
10344
+ const n = rot.items.length;
10345
+ if (n === 0) return -1;
10346
+ if (rot.mode === "shuffle" && n > 1) {
10347
+ let idx = rot.current;
10348
+ while (idx === rot.current) idx = Math.floor(Math.random() * n);
10349
+ return idx;
10350
+ }
10351
+ return (rot.current + 1) % n;
10352
+ }
10353
+ //#endregion
10174
10354
  //#region src/client/index.tsx
10175
10355
  /**
10176
10356
  * dsh-any-background — browser half entry.
@@ -10436,38 +10616,11 @@ window.__ModuleLoader__.load({
10436
10616
  syncMetaNow();
10437
10617
  return true;
10438
10618
  };
10439
- const isoWeekKey = (d) => {
10440
- const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
10441
- const day = t.getUTCDay() || 7;
10442
- t.setUTCDate(t.getUTCDate() + 4 - day);
10443
- const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
10444
- const week = Math.ceil(((t.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
10445
- return `${t.getUTCFullYear()}-W${week}`;
10446
- };
10447
- const rotationDue = () => {
10448
- const rot = rRotation();
10449
- if (!rot.enabled || rot.items.length === 0) return false;
10450
- if (rot.interval === "reload") return true;
10451
- const last = rot.lastRotate !== null ? new Date(rot.lastRotate) : null;
10452
- if (last === null || isNaN(last.getTime())) return true;
10453
- const now = /* @__PURE__ */ new Date();
10454
- if (rot.interval === "daily") return last.toDateString() !== now.toDateString();
10455
- return isoWeekKey(last) !== isoWeekKey(now);
10456
- };
10457
- const pickNextRotationIndex = () => {
10458
- const rot = rRotation();
10459
- const n = rot.items.length;
10460
- if (n === 0) return -1;
10461
- if (rot.mode === "shuffle" && n > 1) {
10462
- let idx = rot.current;
10463
- while (idx === rot.current) idx = Math.floor(Math.random() * n);
10464
- return idx;
10465
- }
10466
- return (rot.current + 1) % n;
10467
- };
10619
+ const rotationDue$1 = () => rotationDue(rRotation());
10620
+ const pickNextRotationIndex$1 = () => pickNextRotationIndex(rRotation());
10468
10621
  const maybeRotate = async () => {
10469
- if (!rotationDue()) return;
10470
- await applyRotationIndex(pickNextRotationIndex(), true);
10622
+ if (!rotationDue$1()) return;
10623
+ await applyRotationIndex(pickNextRotationIndex$1(), true);
10471
10624
  };
10472
10625
  const parseHHMM = (s) => {
10473
10626
  const m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(s);
@@ -10483,6 +10636,7 @@ window.__ModuleLoader__.load({
10483
10636
  return cur >= night && cur < day;
10484
10637
  };
10485
10638
  const scheduleTick = () => {
10639
+ if (rotationAdvancesLive(rRotation())) maybeRotate();
10486
10640
  const sc = rSchedule();
10487
10641
  if (!sc.enabled) return;
10488
10642
  const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ?? false : isNightNow(sc)) ? sc.nightProfile : sc.dayProfile;
@@ -10817,12 +10971,32 @@ window.__ModuleLoader__.load({
10817
10971
  }
10818
10972
  }
10819
10973
  }
10974
+ let pool;
10975
+ const rotItems = rRotation().items;
10976
+ if (rotItems.length > 0) {
10977
+ pool = [];
10978
+ for (const it of rotItems) try {
10979
+ const blob = await fetch(rotationFileUrl(it.file)).then((r) => {
10980
+ if (!r.ok) throw new Error(`http ${r.status}`);
10981
+ return r.blob();
10982
+ });
10983
+ pool.push({
10984
+ file: it.file,
10985
+ thumb: it.thumb,
10986
+ dataUrl: await blobToDataUrl(blob)
10987
+ });
10988
+ } catch {
10989
+ console.warn(`dsh-any-background: export skipped a rotation image (${it.file})`);
10990
+ }
10991
+ if (pool.length === 0) pool = void 0;
10992
+ }
10820
10993
  const payload = {
10821
- version: 2,
10994
+ version: 3,
10822
10995
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
10823
10996
  config: cfg,
10824
10997
  wallpaper: wallpaperPayload,
10825
- video: videoPayload
10998
+ video: videoPayload,
10999
+ pool
10826
11000
  };
10827
11001
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
10828
11002
  const url = URL.createObjectURL(blob);
@@ -10876,6 +11050,26 @@ window.__ModuleLoader__.load({
10876
11050
  if (cfg.regenerateOnReload) regenerateGeneratedBg();
10877
11051
  else if (cfg.generatedBg) updateGeneratedBg(cfg.generatedBg);
10878
11052
  }
11053
+ const poolRaw = d.pool;
11054
+ if (Array.isArray(poolRaw)) {
11055
+ if (poolRaw.length > 0) {
11056
+ const r = await rotationRestore(poolRaw);
11057
+ if (r.ok && r.items) cfg.rotation = {
11058
+ ...cfg.rotation,
11059
+ items: r.items,
11060
+ current: Math.min(cfg.rotation.current, Math.max(0, r.items.length - 1))
11061
+ };
11062
+ else console.warn("dsh-any-background: rotation pool restore failed", r.error);
11063
+ } else if (cfg.rotation.items.length > 0) cfg.rotation = {
11064
+ ...cfg.rotation,
11065
+ items: [],
11066
+ current: 0
11067
+ };
11068
+ } else if ((typeof d.version !== "number" || d.version < 3) && cfg.rotation.items.length > 0) cfg.rotation = {
11069
+ ...cfg.rotation,
11070
+ items: [],
11071
+ current: 0
11072
+ };
10879
11073
  persistConfig();
10880
11074
  syncMetaNow();
10881
11075
  if (rHasColor()) {
@@ -10911,6 +11105,37 @@ window.__ModuleLoader__.load({
10911
11105
  return true;
10912
11106
  },
10913
11107
  applyProfile: (id) => applyProfileById(id),
11108
+ renameProfile: (id, name) => {
11109
+ const trimmed = name.trim();
11110
+ if (!trimmed) return false;
11111
+ const before = rProfiles();
11112
+ if (!before.some((p) => p.id === id)) return false;
11113
+ cfg.profiles = before.map((p) => p.id === id ? {
11114
+ ...p,
11115
+ name: trimmed.slice(0, 60)
11116
+ } : p);
11117
+ persistConfig();
11118
+ syncMetaNow();
11119
+ return true;
11120
+ },
11121
+ duplicateProfile: (id) => {
11122
+ const before = rProfiles();
11123
+ const src = before.find((p) => p.id === id);
11124
+ if (!src) return false;
11125
+ const profiles = [...before];
11126
+ if (profiles.length >= 20) profiles.shift();
11127
+ const nid = `p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
11128
+ profiles.push({
11129
+ ...src,
11130
+ id: nid,
11131
+ name: `${src.name} (2)`.slice(0, 60),
11132
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
11133
+ });
11134
+ cfg.profiles = profiles;
11135
+ persistConfig();
11136
+ syncMetaNow();
11137
+ return true;
11138
+ },
10914
11139
  deleteProfile: (id) => {
10915
11140
  const before = rProfiles();
10916
11141
  if (!before.some((p) => p.id === id)) return false;
@@ -10995,7 +11220,7 @@ window.__ModuleLoader__.load({
10995
11220
  },
10996
11221
  rotateNow: async () => {
10997
11222
  if (rRotation().items.length === 0) return false;
10998
- return applyRotationIndex(pickNextRotationIndex(), true);
11223
+ return applyRotationIndex(pickNextRotationIndex$1(), true);
10999
11224
  },
11000
11225
  setVideoFromUrl: async (url) => {
11001
11226
  const r = await setVideoFromUrl(url);
@@ -11064,10 +11289,10 @@ window.__ModuleLoader__.load({
11064
11289
  }
11065
11290
  }
11066
11291
  };
11067
- let navIconObserver = null;
11292
+ let disposeNavWatch = null;
11068
11293
  const watchNavIcon = () => {
11069
- if (navIconObserver !== null || typeof MutationObserver === "undefined") return;
11070
- navIconObserver = new MutationObserver((records) => {
11294
+ if (disposeNavWatch !== null || typeof MutationObserver === "undefined") return;
11295
+ disposeNavWatch = watchDom((records) => {
11071
11296
  if (records.some((r) => {
11072
11297
  for (const n of r.addedNodes) {
11073
11298
  if (n.nodeType !== 1) continue;
@@ -11077,16 +11302,12 @@ window.__ModuleLoader__.load({
11077
11302
  return false;
11078
11303
  })) applyNavIcon();
11079
11304
  });
11080
- navIconObserver.observe(document.body, {
11081
- childList: true,
11082
- subtree: true
11083
- });
11084
11305
  applyNavIcon();
11085
11306
  };
11086
11307
  watchNavIcon();
11087
11308
  ctx.effect(() => () => {
11088
- navIconObserver?.disconnect();
11089
- navIconObserver = null;
11309
+ disposeNavWatch?.();
11310
+ disposeNavWatch = null;
11090
11311
  }, "dsh-any-background: nav icon watch");
11091
11312
  const restoreSaved = () => {
11092
11313
  if (rHasColor()) {