dsh-any-background 0.3.9 → 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
@@ -1087,6 +1089,10 @@ window.__ModuleLoader__.load({
1087
1089
  {
1088
1090
  channel: "0.2.1",
1089
1091
  release: "0.2.1-rc.2"
1092
+ },
1093
+ {
1094
+ channel: "0.2.1",
1095
+ release: "0.2.1-rc.3"
1090
1096
  }
1091
1097
  ];
1092
1098
  const UNKNOWN_HOST_INFO = {
@@ -1563,7 +1569,7 @@ window.__ModuleLoader__.load({
1563
1569
  //#region src/client/host-compat/versions/v0-2-1/adapter.ts
1564
1570
  /**
1565
1571
  * Adapter for the DSH `0.2.1` patch line, verified at `0.2.1-alpha.1`,
1566
- * `alpha.2`, `alpha.3`, `alpha.4`, `alpha.5`, `rc.1` and `rc.2`.
1572
+ * `alpha.2`, `alpha.3`, `alpha.4`, `alpha.5`, `rc.1`, `rc.2` and `rc.3`.
1567
1573
  *
1568
1574
  * A new MINOR line opened with a prerelease, and the answer is the `0.2.0-rc.2`
1569
1575
  * folder's — which is a statement this file has to earn, because "same as the
@@ -1587,6 +1593,34 @@ window.__ModuleLoader__.load({
1587
1593
  * · `TextShimmer` still marks its root `data-shimmer`, so the stroke exemption
1588
1594
  * written for rc.1 stands.
1589
1595
  *
1596
+ * Re-checked at `0.2.1-rc.3` (`git diff rc.2 rc.3` on every file above), the
1597
+ * facts hold untouched: `design-platform.css`, both menu stylesheets and
1598
+ * `MenuGroup.tsx` byte-identical; `SidebarRight.module.css`, `TabLayout.tsx`,
1599
+ * `base.css`, `gradient-shadow-text.css`, `AppFrame.*`, `InlineEditor.module.css`,
1600
+ * `TextShimmer`, `JobListAction`, `TaskMenu`, `scoped-slots` and
1601
+ * `PluginManagerPage.tsx` all unchanged, and the five header slot keys stand.
1602
+ * What rc.3 did move, and why none of it needs a fact from this folder:
1603
+ * · `ui-primitives/Menu.tsx` is ADDITIVE — a `current` prop that renders
1604
+ * `aria-current="page"` and a `minWidthFromAnchor` switch that sets a
1605
+ * `min-width` style in portal mode. No stylesheet moved with it
1606
+ * (`MenuSurface.module.css` is byte-identical), so the menu paint and the
1607
+ * frost stay exactly where the facts say.
1608
+ * · the guide (`GuideBody.tsx`) and the sidebar panel list (`SidebarRoot.tsx`)
1609
+ * grow pin state and a More menu AROUND the existing registrations — guide
1610
+ * entries still arrive through `useGuideEntries`, panels through
1611
+ * `selectPanel`, and the new menus mount empty until something registers
1612
+ * into their `*.actions` slots, which this plugin does not. The tab
1613
+ * registry itself (`shell/`), which `native-tab.tsx` speaks to, is untouched.
1614
+ * · the session header's View tab row gained its own More menu
1615
+ * (`conversation.session.header.views.actions` slot). It mounts through
1616
+ * `<Menu … portal>` to `document.body`, so `header-tag.ts` already claims
1617
+ * it through the portaled arm — the trigger sits in the header and reports
1618
+ * `aria-expanded`, which is all that arm asks. The five in-place header
1619
+ * slot anchors are unchanged.
1620
+ * · `ConversationRoot.module.css` re-laid the View tab row (wrap, gaps, the
1621
+ * More launcher's classes). Layout inside the header, not a surface the
1622
+ * plugin styles by these class names.
1623
+ *
1590
1624
  * Re-checked again across the rest of the line (`alpha.3` … `rc.2`, `git diff
1591
1625
  * <tag> rc.2` on every file above): the menu token contract, both menu stylesheets
1592
1626
  * (`MenuSurface`, `MenuGroup`), `gradient-shadow-text.css` / `base.css`,
@@ -1678,7 +1712,7 @@ window.__ModuleLoader__.load({
1678
1712
  }
1679
1713
  function createAdapter$1(host) {
1680
1714
  return {
1681
- id: "0.2.1-alpha.1…rc.2",
1715
+ id: "0.2.1-alpha.1…rc.3",
1682
1716
  channel: "0.2.1",
1683
1717
  host,
1684
1718
  panelFragments: panelFragments$1,
@@ -1847,22 +1881,44 @@ window.__ModuleLoader__.load({
1847
1881
  }
1848
1882
  const SAVE_DEBOUNCE_MS = 250;
1849
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
+ }
1850
1906
  function saveConfig() {
1851
1907
  if (saveTimer !== void 0) window.clearTimeout(saveTimer);
1852
1908
  saveTimer = window.setTimeout(() => {
1853
1909
  saveTimer = void 0;
1854
- rpcCall("writeConfig", { config: cfg });
1910
+ writeNow();
1855
1911
  }, SAVE_DEBOUNCE_MS);
1856
1912
  }
1857
1913
  function flushSave() {
1858
1914
  if (saveTimer === void 0) return;
1859
1915
  window.clearTimeout(saveTimer);
1860
1916
  saveTimer = void 0;
1861
- rpcCall("writeConfig", { config: cfg });
1917
+ writeNow();
1862
1918
  }
1863
1919
  /** Persist the current config immediately (import path — no debounce). */
1864
1920
  function persistConfig() {
1865
- rpcCall("writeConfig", { config: cfg });
1921
+ writeNow();
1866
1922
  }
1867
1923
  /** Load the persisted theme (config + wallpaper URL + video URL) from the node
1868
1924
  * half. Image and video both travel as serve URLs — never bytes — so this RPC
@@ -2060,6 +2116,10 @@ window.__ModuleLoader__.load({
2060
2116
  async function removeFont() {
2061
2117
  return await rpcCall("removeFont", {}) === true;
2062
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
+ }
2063
2123
  /** Add an image (data URL + small thumbnail) to the server-side rotation pool. */
2064
2124
  async function rotationAdd(dataUrl, thumb) {
2065
2125
  if (!rpcCallFn) return {
@@ -2133,6 +2193,32 @@ window.__ModuleLoader__.load({
2133
2193
  };
2134
2194
  }
2135
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
+ }
2136
2222
  //#endregion
2137
2223
  //#region src/client/host-compat/capabilities.ts
2138
2224
  /**
@@ -2156,6 +2242,32 @@ window.__ModuleLoader__.load({
2156
2242
  return rHostAdapter();
2157
2243
  }
2158
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
2159
2271
  //#region src/client/header-tag.ts
2160
2272
  /**
2161
2273
  * Header-surface tagging.
@@ -2260,7 +2372,6 @@ window.__ModuleLoader__.load({
2260
2372
  if (el.getAttribute("aria-modal") === "true") return false;
2261
2373
  return el.parentElement === document.body || el.closest(headerSlotAnchors()) !== null;
2262
2374
  }
2263
- let observer = null;
2264
2375
  let pending = null;
2265
2376
  /** False once the effect is torn down, so a pass already queued on the microtask queue
2266
2377
  * cannot re-mark a popover after the teardown cleared it. */
@@ -2301,17 +2412,10 @@ window.__ModuleLoader__.load({
2301
2412
  if (typeof MutationObserver === "undefined" || typeof document === "undefined") return () => {};
2302
2413
  active = true;
2303
2414
  refreshHeaderPopovers();
2304
- observer = new MutationObserver(schedule);
2305
- observer.observe(document.body, {
2306
- childList: true,
2307
- subtree: true,
2308
- attributes: true,
2309
- attributeFilter: ["aria-expanded"]
2310
- });
2415
+ const dispose = watchDom(() => schedule());
2311
2416
  return () => {
2312
2417
  active = false;
2313
- observer?.disconnect();
2314
- observer = null;
2418
+ dispose();
2315
2419
  for (const el of document.querySelectorAll(`[${HEADER_POPOVER_ATTR}]`)) el.removeAttribute(HEADER_POPOVER_ATTR);
2316
2420
  };
2317
2421
  }
@@ -4734,7 +4838,8 @@ window.__ModuleLoader__.load({
4734
4838
  *
4735
4839
  * Bound by the CSS-Module class shape, same as `[class*="_ioCard"]`: the built
4736
4840
  * bundle names these `pI_x6G_sidebarCol` — prefix first, local name last, and the
4737
- * pair is identical across `0.2.0-rc.1`, `0.2.1-alpha.3` and `0.2.1-rc.2`. A build
4841
+ * pair is identical across `0.2.0-rc.1`, `0.2.1-alpha.3`, `0.2.1-rc.2` and
4842
+ * `0.2.1-rc.3` (`AppFrame.module.css` byte-identical rc.2→rc.3). A build
4738
4843
  * that renames the track simply stops matching, which is the untouched host look. */
4739
4844
  const SIDEBAR_ONE_LAYER_RULE = "[class*=\"_sidebarCol\"] *{--dsw-specific-sidebar-fill:transparent}";
4740
4845
  const PART_BLUR_RULE = `.${PART_BLUR_CLASS}{isolation:isolate}.${PART_BLUR_CLASS}:has([role="dialog"]){isolation:auto}.${PART_UNDERLAY_CLASS}{position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;backdrop-filter:var(--dsh-any-part-blur,none);-webkit-backdrop-filter:var(--dsh-any-part-blur,none)}`;
@@ -5143,8 +5248,11 @@ window.__ModuleLoader__.load({
5143
5248
  });
5144
5249
  syncTableFix();
5145
5250
  }
5146
- let partsObserver = null;
5147
- 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;
5148
5256
  /** Signature of the last blur+opacity pass we actually wrote to the DOM. */
5149
5257
  let appliedPartsKey = "";
5150
5258
  /** The surfaces the last pass styled, compared BY IDENTITY: the host can swap
@@ -5184,11 +5292,13 @@ window.__ModuleLoader__.load({
5184
5292
  /** Watch for the AppFrame mounting so persisted blurs land even when the shell
5185
5293
  * renders after this plugin's apply. */
5186
5294
  function watchParts() {
5187
- if (partsObserver !== null || typeof MutationObserver === "undefined") return;
5188
- partsObserver = new MutationObserver(() => {
5189
- if (partsApplyRaf !== 0) return;
5190
- partsApplyRaf = requestAnimationFrame(() => {
5191
- 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;
5192
5302
  const blurs = rBlurs();
5193
5303
  const ops = rOps();
5194
5304
  const key = JSON.stringify([blurs, ops]);
@@ -5199,18 +5309,12 @@ window.__ModuleLoader__.load({
5199
5309
  appliedTargets = targetsNow();
5200
5310
  });
5201
5311
  });
5202
- partsObserver.observe(document.body, {
5203
- childList: true,
5204
- subtree: true
5205
- });
5206
5312
  }
5207
5313
  function stopWatchingParts() {
5208
- partsObserver?.disconnect();
5209
- partsObserver = null;
5210
- if (partsApplyRaf !== 0) {
5211
- cancelAnimationFrame(partsApplyRaf);
5212
- partsApplyRaf = 0;
5213
- }
5314
+ partsDispose?.();
5315
+ partsDispose = null;
5316
+ partsActive = false;
5317
+ partsPending = null;
5214
5318
  appliedPartsKey = "";
5215
5319
  appliedTargets = [];
5216
5320
  markerSeen.clear();
@@ -5523,8 +5627,6 @@ window.__ModuleLoader__.load({
5523
5627
  applyTrajectoryOverrides(rTrajectoryOpacity());
5524
5628
  }
5525
5629
  applyPanelOverrides(rPanelOpacity());
5526
- applyProduced();
5527
- applyHeaderPopovers();
5528
5630
  applyExemptDefaults();
5529
5631
  applyPartBlurs(rBlurs());
5530
5632
  applyStrokes();
@@ -8760,6 +8862,12 @@ window.__ModuleLoader__.load({
8760
8862
  onClick: () => setRotation({ interval: "reload" }),
8761
8863
  children: t("rotReload")
8762
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
+ }),
8763
8871
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8764
8872
  type: "button",
8765
8873
  className: `dab-chip${rotation.interval === "daily" ? " is-active" : ""}`,
@@ -9289,7 +9397,7 @@ window.__ModuleLoader__.load({
9289
9397
  });
9290
9398
  }
9291
9399
  function ProfilePage({ p, notify }) {
9292
- 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;
9293
9401
  const profiles = useStore((s) => s.profiles);
9294
9402
  const schedule = useStore((s) => s.schedule);
9295
9403
  const activeProfile = useStore((s) => s.activeProfile);
@@ -9297,6 +9405,8 @@ window.__ModuleLoader__.load({
9297
9405
  const [nameOpen, setNameOpen] = (0, react.useState)(false);
9298
9406
  const [nameVal, setNameVal] = (0, react.useState)("");
9299
9407
  const [confirmId, setConfirmId] = (0, react.useState)(null);
9408
+ const [editId, setEditId] = (0, react.useState)(null);
9409
+ const [editVal, setEditVal] = (0, react.useState)("");
9300
9410
  const onImport = async (file) => {
9301
9411
  try {
9302
9412
  const ok = await importTheme(file);
@@ -9325,6 +9435,14 @@ window.__ModuleLoader__.load({
9325
9435
  setConfirmId(null);
9326
9436
  deleteProfile(entry.id);
9327
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
+ };
9328
9446
  const profileOptions = (value, onChange) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
9329
9447
  className: "dab-select",
9330
9448
  value: value ?? "",
@@ -9456,8 +9574,42 @@ window.__ModuleLoader__.load({
9456
9574
  children: profiles.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9457
9575
  className: `dab-profile-row${activeProfile === entry.id ? " is-active" : ""}`,
9458
9576
  style: { "--i": i },
9459
- children: [
9460
- /* @__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: [
9461
9613
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9462
9614
  className: "dab-profile-meta",
9463
9615
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -9474,6 +9626,23 @@ window.__ModuleLoader__.load({
9474
9626
  onClick: () => onApply(entry),
9475
9627
  children: t("profileApply")
9476
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
+ }),
9477
9646
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9478
9647
  type: "button",
9479
9648
  className: `dab-btn dab-btn-danger-solid${confirmId === entry.id ? " dab-btn-confirm" : ""}`,
@@ -9481,7 +9650,7 @@ window.__ModuleLoader__.load({
9481
9650
  onClick: () => onDelete(entry),
9482
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")
9483
9652
  })
9484
- ]
9653
+ ] })]
9485
9654
  }, entry.id))
9486
9655
  })
9487
9656
  ]
@@ -10015,9 +10184,9 @@ window.__ModuleLoader__.load({
10015
10184
  * unsubscribe for plugin teardown. */
10016
10185
  function startBetterSidebarWatch() {
10017
10186
  sync();
10018
- const observer = new MutationObserver(sync);
10019
- observer.observe(document.body, { childList: true });
10020
- return () => observer.disconnect();
10187
+ return watchDom((records) => {
10188
+ if (records.some((r) => r.target === document.body && r.type === "childList")) sync();
10189
+ });
10021
10190
  }
10022
10191
  let nativeTabs = false;
10023
10192
  const nativeSubs = /* @__PURE__ */ new Set();
@@ -10138,6 +10307,50 @@ window.__ModuleLoader__.load({
10138
10307
  });
10139
10308
  }
10140
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
10141
10354
  //#region src/client/index.tsx
10142
10355
  /**
10143
10356
  * dsh-any-background — browser half entry.
@@ -10403,38 +10616,11 @@ window.__ModuleLoader__.load({
10403
10616
  syncMetaNow();
10404
10617
  return true;
10405
10618
  };
10406
- const isoWeekKey = (d) => {
10407
- const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
10408
- const day = t.getUTCDay() || 7;
10409
- t.setUTCDate(t.getUTCDate() + 4 - day);
10410
- const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
10411
- const week = Math.ceil(((t.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
10412
- return `${t.getUTCFullYear()}-W${week}`;
10413
- };
10414
- const rotationDue = () => {
10415
- const rot = rRotation();
10416
- if (!rot.enabled || rot.items.length === 0) return false;
10417
- if (rot.interval === "reload") return true;
10418
- const last = rot.lastRotate !== null ? new Date(rot.lastRotate) : null;
10419
- if (last === null || isNaN(last.getTime())) return true;
10420
- const now = /* @__PURE__ */ new Date();
10421
- if (rot.interval === "daily") return last.toDateString() !== now.toDateString();
10422
- return isoWeekKey(last) !== isoWeekKey(now);
10423
- };
10424
- const pickNextRotationIndex = () => {
10425
- const rot = rRotation();
10426
- const n = rot.items.length;
10427
- if (n === 0) return -1;
10428
- if (rot.mode === "shuffle" && n > 1) {
10429
- let idx = rot.current;
10430
- while (idx === rot.current) idx = Math.floor(Math.random() * n);
10431
- return idx;
10432
- }
10433
- return (rot.current + 1) % n;
10434
- };
10619
+ const rotationDue$1 = () => rotationDue(rRotation());
10620
+ const pickNextRotationIndex$1 = () => pickNextRotationIndex(rRotation());
10435
10621
  const maybeRotate = async () => {
10436
- if (!rotationDue()) return;
10437
- await applyRotationIndex(pickNextRotationIndex(), true);
10622
+ if (!rotationDue$1()) return;
10623
+ await applyRotationIndex(pickNextRotationIndex$1(), true);
10438
10624
  };
10439
10625
  const parseHHMM = (s) => {
10440
10626
  const m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(s);
@@ -10450,6 +10636,7 @@ window.__ModuleLoader__.load({
10450
10636
  return cur >= night && cur < day;
10451
10637
  };
10452
10638
  const scheduleTick = () => {
10639
+ if (rotationAdvancesLive(rRotation())) maybeRotate();
10453
10640
  const sc = rSchedule();
10454
10641
  if (!sc.enabled) return;
10455
10642
  const want = (sc.mode === "system" ? window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ?? false : isNightNow(sc)) ? sc.nightProfile : sc.dayProfile;
@@ -10784,12 +10971,32 @@ window.__ModuleLoader__.load({
10784
10971
  }
10785
10972
  }
10786
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
+ }
10787
10993
  const payload = {
10788
- version: 2,
10994
+ version: 3,
10789
10995
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
10790
10996
  config: cfg,
10791
10997
  wallpaper: wallpaperPayload,
10792
- video: videoPayload
10998
+ video: videoPayload,
10999
+ pool
10793
11000
  };
10794
11001
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
10795
11002
  const url = URL.createObjectURL(blob);
@@ -10843,6 +11050,26 @@ window.__ModuleLoader__.load({
10843
11050
  if (cfg.regenerateOnReload) regenerateGeneratedBg();
10844
11051
  else if (cfg.generatedBg) updateGeneratedBg(cfg.generatedBg);
10845
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
+ };
10846
11073
  persistConfig();
10847
11074
  syncMetaNow();
10848
11075
  if (rHasColor()) {
@@ -10878,6 +11105,37 @@ window.__ModuleLoader__.load({
10878
11105
  return true;
10879
11106
  },
10880
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
+ },
10881
11139
  deleteProfile: (id) => {
10882
11140
  const before = rProfiles();
10883
11141
  if (!before.some((p) => p.id === id)) return false;
@@ -10962,7 +11220,7 @@ window.__ModuleLoader__.load({
10962
11220
  },
10963
11221
  rotateNow: async () => {
10964
11222
  if (rRotation().items.length === 0) return false;
10965
- return applyRotationIndex(pickNextRotationIndex(), true);
11223
+ return applyRotationIndex(pickNextRotationIndex$1(), true);
10966
11224
  },
10967
11225
  setVideoFromUrl: async (url) => {
10968
11226
  const r = await setVideoFromUrl(url);
@@ -11031,10 +11289,10 @@ window.__ModuleLoader__.load({
11031
11289
  }
11032
11290
  }
11033
11291
  };
11034
- let navIconObserver = null;
11292
+ let disposeNavWatch = null;
11035
11293
  const watchNavIcon = () => {
11036
- if (navIconObserver !== null || typeof MutationObserver === "undefined") return;
11037
- navIconObserver = new MutationObserver((records) => {
11294
+ if (disposeNavWatch !== null || typeof MutationObserver === "undefined") return;
11295
+ disposeNavWatch = watchDom((records) => {
11038
11296
  if (records.some((r) => {
11039
11297
  for (const n of r.addedNodes) {
11040
11298
  if (n.nodeType !== 1) continue;
@@ -11044,16 +11302,12 @@ window.__ModuleLoader__.load({
11044
11302
  return false;
11045
11303
  })) applyNavIcon();
11046
11304
  });
11047
- navIconObserver.observe(document.body, {
11048
- childList: true,
11049
- subtree: true
11050
- });
11051
11305
  applyNavIcon();
11052
11306
  };
11053
11307
  watchNavIcon();
11054
11308
  ctx.effect(() => () => {
11055
- navIconObserver?.disconnect();
11056
- navIconObserver = null;
11309
+ disposeNavWatch?.();
11310
+ disposeNavWatch = null;
11057
11311
  }, "dsh-any-background: nav icon watch");
11058
11312
  const restoreSaved = () => {
11059
11313
  if (rHasColor()) {