dsh-any-background 0.1.8 → 0.1.9

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
@@ -20,7 +20,7 @@ window.__ModuleLoader__.load({
20
20
  pageProfile: "配置",
21
21
  descColor: "拖动色轮或输入精确数值,整个界面的配色将实时跟随变化",
22
22
  descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感",
23
- descBackground: "上传一张图片作为壁纸,或让算法为你实时生成动态背景",
23
+ descBackground: "上传一张图片或一段视频作为壁纸并自由摆放,或让算法为你实时生成动态背景",
24
24
  descProfile: "将当前主题导出为文件备份,或从文件一键恢复完整配置",
25
25
  colorTitle: "主题色",
26
26
  colorHint: "在色轮外圈选择色相,在内部方形中调整饱和度和明度;双击滑块可恢复默认值",
@@ -33,19 +33,29 @@ window.__ModuleLoader__.load({
33
33
  uiOpacitySide: "侧边栏",
34
34
  uiOpacityCard: "对话框中选项面板",
35
35
  uiSop: "设置界面透明度",
36
+ uiChatRegion: "对话文本框",
37
+ uiTrajectory: "轨迹页",
36
38
  bgTitle: "背景",
37
- bgChoose: "选择图片",
39
+ bgChoose: "选择图片或视频",
38
40
  bgRemove: "移除背景",
39
41
  bgEdit: "编辑位置",
42
+ bgEditLocked: "仅「适应」模式可编辑位置",
40
43
  wpOpacity: "背景透明度",
41
44
  bgBlur: "背景模糊",
42
- bgSourceImage: "图片",
45
+ bgModeTitle: "布局模式",
46
+ bgModeFit: "适应",
47
+ bgModeFill: "填充",
48
+ bgModeStretch: "拉伸",
49
+ bgModeTile: "平铺",
50
+ bgModeCenter: "居中",
51
+ bgVideoBadge: "视频",
52
+ bgSourceImage: "图片 / 视频",
43
53
  bgSourceGenerated: "动态生成",
44
- dropHint: "点击选择图片,或将图片拖到这里",
54
+ dropHint: "点击选择图片或视频,或将文件拖到这里",
45
55
  liveBadge: "动态壁纸",
46
- bgHint: "图片模式下悬停预览可编辑位置;动态生成模式下可选择类型、预设与参数。绑定种子后刷新页面壁纸保持不变,取消绑定则每次刷新随机换新",
56
+ bgHint: "「适应」布局模式下悬停预览可编辑图片/视频的位置与缩放(其余布局模式按模式自动排布);动态生成模式下可选择类型、预设与参数。绑定种子后刷新页面壁纸保持不变,取消绑定则每次刷新随机换新",
47
57
  editorTitle: "背景编辑器",
48
- editorHint: "拖动移动图片,滚轮缩放大小",
58
+ editorHint: "拖动移动画面,滚轮缩放大小",
49
59
  editorCommit: "确认",
50
60
  editorCancel: "取消",
51
61
  editorReset: "重置",
@@ -107,7 +117,7 @@ window.__ModuleLoader__.load({
107
117
  pageProfile: "Profile",
108
118
  descColor: "Drag the wheel or type exact values — the whole UI follows live",
109
119
  descInterface: "Tune opacity and blur per surface to build depth",
110
- descBackground: "Upload an image as wallpaper, or let the algorithm paint a live background",
120
+ descBackground: "Upload an image or a video as wallpaper and place it freely, or let the algorithm paint a live background",
111
121
  descProfile: "Export the current theme as a file, or restore it from one",
112
122
  colorTitle: "Theme color",
113
123
  colorHint: "Pick hue on the outer ring, adjust saturation & lightness in the square; double-click a slider to reset",
@@ -120,17 +130,27 @@ window.__ModuleLoader__.load({
120
130
  uiOpacitySide: "Sidebar",
121
131
  uiOpacityCard: "Cards & panels",
122
132
  uiSop: "Settings interface opacity",
133
+ uiChatRegion: "Conversation text frame",
134
+ uiTrajectory: "Trajectory view",
123
135
  bgTitle: "Background",
124
- bgChoose: "Choose image",
136
+ bgChoose: "Choose image or video",
125
137
  bgRemove: "Remove background",
126
138
  bgEdit: "Edit position",
139
+ bgEditLocked: "Position editing is only available in Fit mode",
127
140
  wpOpacity: "Background opacity",
128
141
  bgBlur: "Background blur",
129
- bgSourceImage: "Image",
142
+ bgModeTitle: "Layout mode",
143
+ bgModeFit: "Fit",
144
+ bgModeFill: "Fill",
145
+ bgModeStretch: "Stretch",
146
+ bgModeTile: "Tile",
147
+ bgModeCenter: "Center",
148
+ bgVideoBadge: "Video",
149
+ bgSourceImage: "Image / Video",
130
150
  bgSourceGenerated: "Generated",
131
- dropHint: "Click to choose an image, or drop one here",
151
+ dropHint: "Click to choose an image or video, or drop one here",
132
152
  liveBadge: "Live wallpaper",
133
- bgHint: "In image mode hover the preview to edit position; in generated mode pick a type, preset and parameters. With the seed locked the wallpaper stays the same across refreshes; unlocked, every refresh randomizes it",
153
+ bgHint: "In Fit layout mode hover the preview to reposition and zoom the image/video (other modes arrange it automatically); in generated mode pick a type, preset and parameters. With the seed locked the wallpaper stays the same across refreshes; unlocked, every refresh randomizes it",
134
154
  editorTitle: "Background editor",
135
155
  editorHint: "Drag to move, scroll to zoom",
136
156
  editorCommit: "Confirm",
@@ -197,7 +217,9 @@ window.__ModuleLoader__.load({
197
217
  bg: 0,
198
218
  sidebar: 0,
199
219
  card: 0,
200
- settings: 0
220
+ settings: 0,
221
+ chat: 0,
222
+ trajectory: 0
201
223
  },
202
224
  settingsOpacity: 1,
203
225
  wallpaperOpacity: 1,
@@ -209,24 +231,54 @@ window.__ModuleLoader__.load({
209
231
  iw: 0,
210
232
  ih: 0
211
233
  },
234
+ videoBgState: {
235
+ zoom: 1,
236
+ x: 0,
237
+ y: 0,
238
+ iw: 0,
239
+ ih: 0
240
+ },
212
241
  backgroundType: "image",
242
+ bgMode: "fit",
243
+ videoMime: null,
213
244
  generatedBg: null,
214
- regenerateOnReload: false
245
+ regenerateOnReload: false,
246
+ chatTextOpacity: 0,
247
+ trajectoryOpacity: 1
215
248
  };
249
+ const clamp01 = (n, def) => typeof n === "number" ? Math.min(1, Math.max(0, n)) : def;
216
250
  let cfg = {
217
251
  ...DEFAULT_CONFIG,
218
252
  opacities: { ...DEFAULT_CONFIG.opacities },
219
253
  blurs: { ...DEFAULT_CONFIG.blurs },
220
- bgState: { ...DEFAULT_CONFIG.bgState }
254
+ bgState: { ...DEFAULT_CONFIG.bgState },
255
+ videoBgState: { ...DEFAULT_CONFIG.videoBgState }
221
256
  };
222
257
  let wpImageUrl = null;
223
258
  let wpUrl = null;
259
+ let wpVideoUrl = null;
260
+ /** Captured video frame standing in for previews/color extraction (still-image APIs). */
261
+ let wpVideoSnapshot = null;
224
262
  function setWpImageUrl(url) {
225
263
  wpImageUrl = url;
226
264
  }
227
265
  function setWpUrl(url) {
228
266
  wpUrl = url;
229
267
  }
268
+ let videoRev = 0;
269
+ function setWpVideoUrl(url, mime) {
270
+ if (url === null) {
271
+ wpVideoUrl = null;
272
+ wpVideoSnapshot = null;
273
+ } else {
274
+ videoRev++;
275
+ wpVideoUrl = `${url}${url.includes("?") ? "&" : "?"}r=${videoRev}`;
276
+ }
277
+ cfg.videoMime = url ? mime : null;
278
+ }
279
+ function setWpVideoSnapshot(url) {
280
+ wpVideoSnapshot = url;
281
+ }
230
282
  function setBgState(s) {
231
283
  cfg.bgState = s;
232
284
  }
@@ -250,39 +302,76 @@ window.__ModuleLoader__.load({
250
302
  function rWpImage() {
251
303
  return wpImageUrl;
252
304
  }
253
- /** Display URL: the uploaded image when in image mode, else the generated snapshot. */
305
+ function rWpVideo() {
306
+ return wpVideoUrl;
307
+ }
308
+ function rBgMode() {
309
+ return cfg.bgMode ?? DEFAULT_CONFIG.bgMode;
310
+ }
311
+ function rChatTextOpacity() {
312
+ return clamp01(cfg.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity);
313
+ }
314
+ function rTrajectoryOpacity() {
315
+ return clamp01(cfg.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity);
316
+ }
317
+ /** Display URL: the uploaded image/video snapshot per active type, else the generated snapshot. */
254
318
  function rWp() {
255
- return cfg.backgroundType === "image" ? wpImageUrl : wpUrl;
319
+ if (cfg.backgroundType === "image") return wpImageUrl;
320
+ if (cfg.backgroundType === "video") return wpVideoSnapshot;
321
+ return wpUrl;
256
322
  }
257
323
  function rOps() {
258
324
  const o = cfg.opacities ?? {};
259
- return {
260
- bg: typeof o.bg === "number" ? Math.min(1, Math.max(0, o.bg)) : DEFAULT_CONFIG.opacities.bg,
261
- sidebar: typeof o.sidebar === "number" ? Math.min(1, Math.max(0, o.sidebar)) : DEFAULT_CONFIG.opacities.sidebar,
262
- card: typeof o.card === "number" ? Math.min(1, Math.max(0, o.card)) : DEFAULT_CONFIG.opacities.card
263
- };
325
+ const out = {};
326
+ for (const k of [
327
+ "bg",
328
+ "sidebar",
329
+ "card"
330
+ ]) out[k] = clamp01(o[k], DEFAULT_CONFIG.opacities[k]);
331
+ return out;
264
332
  }
265
333
  function rBlurs() {
266
334
  const b = cfg.blurs ?? {};
267
- return {
268
- bg: typeof b.bg === "number" ? Math.min(60, Math.max(0, b.bg)) : DEFAULT_CONFIG.blurs.bg,
269
- sidebar: typeof b.sidebar === "number" ? Math.min(60, Math.max(0, b.sidebar)) : DEFAULT_CONFIG.blurs.sidebar,
270
- card: typeof b.card === "number" ? Math.min(60, Math.max(0, b.card)) : DEFAULT_CONFIG.blurs.card,
271
- settings: typeof b.settings === "number" ? Math.min(60, Math.max(0, b.settings)) : DEFAULT_CONFIG.blurs.settings
272
- };
335
+ const out = {};
336
+ for (const k of [
337
+ "bg",
338
+ "sidebar",
339
+ "card",
340
+ "settings",
341
+ "chat",
342
+ "trajectory"
343
+ ]) {
344
+ const v = b[k];
345
+ out[k] = typeof v === "number" ? Math.min(60, Math.max(0, v)) : DEFAULT_CONFIG.blurs[k];
346
+ }
347
+ return out;
273
348
  }
274
349
  function rWop() {
275
- return typeof cfg.wallpaperOpacity === "number" ? Math.min(1, Math.max(0, cfg.wallpaperOpacity)) : DEFAULT_CONFIG.wallpaperOpacity;
350
+ return clamp01(cfg.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity);
276
351
  }
277
352
  function rBl() {
278
353
  return typeof cfg.blur === "number" ? Math.min(60, Math.max(0, cfg.blur)) : DEFAULT_CONFIG.blur;
279
354
  }
280
355
  function rSop() {
281
- return typeof cfg.settingsOpacity === "number" ? Math.min(1, Math.max(0, cfg.settingsOpacity)) : DEFAULT_CONFIG.settingsOpacity;
356
+ return clamp01(cfg.settingsOpacity, DEFAULT_CONFIG.settingsOpacity);
282
357
  }
283
358
  function rBgState() {
284
359
  return cfg.bgState;
285
360
  }
361
+ function rVideoBgState() {
362
+ return cfg.videoBgState;
363
+ }
364
+ const num = (n, def) => typeof n === "number" ? n : def;
365
+ const cl = (n, lo, hi, def) => typeof n === "number" ? Math.min(hi, Math.max(lo, n)) : def;
366
+ function adoptBgState(s) {
367
+ return {
368
+ zoom: num(s.zoom, 1),
369
+ x: num(s.x, 0),
370
+ y: num(s.y, 0),
371
+ iw: typeof s.iw === "number" && s.iw > 0 ? s.iw : 0,
372
+ ih: typeof s.ih === "number" && s.ih > 0 ? s.ih : 0
373
+ };
374
+ }
286
375
  /** Move a possibly-absent partial config into the shape the UI reads. */
287
376
  function adoptConfig(raw) {
288
377
  const c = raw ?? {};
@@ -291,48 +380,62 @@ window.__ModuleLoader__.load({
291
380
  c.color[1],
292
381
  c.color[2]
293
382
  ] : null;
294
- const bg = c.bgState ?? {};
295
383
  const legacy = typeof c.opacity === "number" ? c.opacity : null;
296
384
  const ops = c.opacities ?? {};
297
385
  const bl = c.blurs ?? {};
298
- const bgType = c.backgroundType === "mesh" || c.backgroundType === "shader" || c.backgroundType === "pattern" ? c.backgroundType : DEFAULT_CONFIG.backgroundType;
386
+ const blurs = {};
387
+ for (const k of [
388
+ "bg",
389
+ "sidebar",
390
+ "card",
391
+ "settings",
392
+ "chat",
393
+ "trajectory"
394
+ ]) blurs[k] = num(bl[k], DEFAULT_CONFIG.blurs[k]);
395
+ const bgType = [
396
+ "video",
397
+ "mesh",
398
+ "shader",
399
+ "pattern"
400
+ ].includes(c.backgroundType) ? c.backgroundType : DEFAULT_CONFIG.backgroundType;
401
+ const bgMode = [
402
+ "fit",
403
+ "fill",
404
+ "stretch",
405
+ "tile",
406
+ "center"
407
+ ].includes(c.bgMode) ? c.bgMode : DEFAULT_CONFIG.bgMode;
299
408
  const gen = c.generatedBg && typeof c.generatedBg === "object" ? c.generatedBg : null;
300
409
  const generatedBg = gen && gen.type === bgType ? c.generatedBg : null;
301
410
  cfg = {
302
411
  color,
303
412
  opacities: {
304
- bg: typeof ops.bg === "number" ? ops.bg : legacy ?? DEFAULT_CONFIG.opacities.bg,
305
- sidebar: typeof ops.sidebar === "number" ? ops.sidebar : legacy !== null ? Math.min(1, legacy + .08) : DEFAULT_CONFIG.opacities.sidebar,
306
- card: typeof ops.card === "number" ? ops.card : DEFAULT_CONFIG.opacities.card
307
- },
308
- blurs: {
309
- bg: typeof bl.bg === "number" ? bl.bg : DEFAULT_CONFIG.blurs.bg,
310
- sidebar: typeof bl.sidebar === "number" ? bl.sidebar : DEFAULT_CONFIG.blurs.sidebar,
311
- card: typeof bl.card === "number" ? bl.card : DEFAULT_CONFIG.blurs.card,
312
- settings: typeof bl.settings === "number" ? bl.settings : DEFAULT_CONFIG.blurs.settings
313
- },
314
- settingsOpacity: typeof c.settingsOpacity === "number" ? c.settingsOpacity : DEFAULT_CONFIG.settingsOpacity,
315
- wallpaperOpacity: typeof c.wallpaperOpacity === "number" ? c.wallpaperOpacity : DEFAULT_CONFIG.wallpaperOpacity,
316
- blur: typeof c.blur === "number" ? c.blur : DEFAULT_CONFIG.blur,
317
- bgState: {
318
- zoom: typeof bg.zoom === "number" ? bg.zoom : 1,
319
- x: typeof bg.x === "number" ? bg.x : 0,
320
- y: typeof bg.y === "number" ? bg.y : 0,
321
- iw: typeof bg.iw === "number" && bg.iw > 0 ? bg.iw : 0,
322
- ih: typeof bg.ih === "number" && bg.ih > 0 ? bg.ih : 0
413
+ bg: num(ops.bg, legacy ?? DEFAULT_CONFIG.opacities.bg),
414
+ sidebar: num(ops.sidebar, legacy !== null ? Math.min(1, legacy + .08) : DEFAULT_CONFIG.opacities.sidebar),
415
+ card: num(ops.card, DEFAULT_CONFIG.opacities.card)
323
416
  },
417
+ blurs,
418
+ settingsOpacity: num(c.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
419
+ wallpaperOpacity: num(c.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity),
420
+ blur: num(c.blur, DEFAULT_CONFIG.blur),
421
+ bgState: adoptBgState(c.bgState ?? {}),
422
+ videoBgState: adoptBgState(c.videoBgState ?? {}),
324
423
  backgroundType: bgType,
424
+ bgMode,
425
+ videoMime: typeof c.videoMime === "string" ? c.videoMime : null,
325
426
  generatedBg: generatedBg ? normalizeGeneratedBg(generatedBg) : null,
326
- regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload
427
+ regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
428
+ chatTextOpacity: clamp01(c.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
429
+ trajectoryOpacity: clamp01(c.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity)
327
430
  };
328
431
  }
329
432
  function normalizeGeneratedBg(p) {
330
433
  if (!p) return null;
331
434
  if (p.type === "mesh") return {
332
435
  type: "mesh",
333
- seed: typeof p.seed === "number" ? p.seed : 0,
334
- scale: typeof p.scale === "number" ? Math.min(3, Math.max(.3, p.scale)) : 1,
335
- intensity: typeof p.intensity === "number" ? Math.min(1, Math.max(0, p.intensity)) : .6
436
+ seed: num(p.seed, 0),
437
+ scale: cl(p.scale, .3, 3, 1),
438
+ intensity: cl(p.intensity, 0, 1, .6)
336
439
  };
337
440
  if (p.type === "shader") return {
338
441
  type: "shader",
@@ -341,8 +444,8 @@ window.__ModuleLoader__.load({
341
444
  "nebula",
342
445
  "noise"
343
446
  ].includes(p.preset) ? p.preset : "aurora",
344
- speed: typeof p.speed === "number" ? Math.min(2, Math.max(0, p.speed)) : .3,
345
- scale: typeof p.scale === "number" ? Math.min(3, Math.max(.3, p.scale)) : 1,
447
+ speed: cl(p.speed, 0, 2, .3),
448
+ scale: cl(p.scale, .3, 3, 1),
346
449
  seed: typeof p.seed === "number" ? Math.floor(p.seed) : 0
347
450
  };
348
451
  return {
@@ -352,14 +455,18 @@ window.__ModuleLoader__.load({
352
455
  "waves",
353
456
  "poly"
354
457
  ].includes(p.preset) ? p.preset : "dots",
355
- density: typeof p.density === "number" ? Math.min(1, Math.max(0, p.density)) : .5,
356
- scale: typeof p.scale === "number" ? Math.min(3, Math.max(.3, p.scale)) : 1,
458
+ density: cl(p.density, 0, 1, .5),
459
+ scale: cl(p.scale, .3, 3, 1),
357
460
  seed: typeof p.seed === "number" ? Math.floor(p.seed) : 0
358
461
  };
359
462
  }
360
463
  //#endregion
361
464
  //#region src/client/rpc.ts
362
465
  const RPC_CHANNEL = "/dsh-any-background";
466
+ /** Same-origin serve URL of the persisted video (enough for <video src>/fetch). */
467
+ const VIDEO_SERVE_URL = "/dsh-any-background/video";
468
+ /** HTTP route new videos are POSTed to as raw bytes (see uploadVideo). */
469
+ const VIDEO_UPLOAD_URL = "/dsh-any-background/video/upload";
363
470
  const RPC_NS = "dshAnyBackground";
364
471
  const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
365
472
  let rpcCallFn = null;
@@ -397,7 +504,7 @@ window.__ModuleLoader__.load({
397
504
  function persistConfig() {
398
505
  rpcCall("writeConfig", { config: cfg });
399
506
  }
400
- /** Load the persisted theme (config + wallpaper) from the node half. */
507
+ /** Load the persisted theme (config + wallpaper + video URL) from the node half. */
401
508
  async function loadPersisted() {
402
509
  const data = await rpcCall("read", {});
403
510
  if (data && typeof data === "object") {
@@ -405,13 +512,34 @@ window.__ModuleLoader__.load({
405
512
  if (d.config) adoptConfig(d.config);
406
513
  if (typeof d.wallpaper === "string") setWpImageUrl(d.wallpaper);
407
514
  else if (d.wallpaper === null) setWpImageUrl(null);
515
+ if (typeof d.videoUrl === "string") setWpVideoUrl(d.videoUrl, cfg.videoMime);
516
+ else if (d.videoUrl === null) setWpVideoUrl(null, null);
408
517
  if (cfg.backgroundType === "image") setWpUrl(d.wallpaper === null ? null : d.wallpaper);
409
518
  }
410
519
  }
411
- /** Persist a wallpaper (null removes it). One-shot large payload, no debounce. */
520
+ /** Persist a wallpaper (null removes it); one-shot, no debounce. */
412
521
  function persistWallpaper(dataUrl) {
413
522
  rpcCall("setWallpaper", { dataUrl });
414
523
  }
524
+ /** Persist a background video (null removes it); resolves true once on disk,
525
+ * so callers only switch playback to the serve URL after acceptance. */
526
+ async function persistVideo(dataUrl) {
527
+ return await rpcCall("setVideo", { dataUrl }) === true;
528
+ }
529
+ /** Upload a video's raw bytes over HTTP (MIME in Content-Type, body untouched
530
+ * — no base64 inflation that would blow the RPC body limit on large clips). */
531
+ async function uploadVideo(blob, mime) {
532
+ try {
533
+ return (await fetch(VIDEO_UPLOAD_URL, {
534
+ method: "POST",
535
+ headers: { "Content-Type": mime || "application/octet-stream" },
536
+ body: blob
537
+ })).ok;
538
+ } catch (e) {
539
+ console.warn("dsh-any-background: video upload failed", e);
540
+ return false;
541
+ }
542
+ }
415
543
  //#endregion
416
544
  //#region src/client/utils/color.ts
417
545
  function hsvToHsl(h, s, v) {
@@ -437,13 +565,8 @@ window.__ModuleLoader__.load({
437
565
  * same token set, and applyWp / applyCustomTokens / applySettingsOverrides call
438
566
  * this repeatedly (slider drags, viewport re-applies), so cache the last result
439
567
  * and skip the 30+ hsl() string builds when nothing changed.
440
- *
441
- * This is the simple, pre-Material-You version: dark/light is decided only by
442
- * lit < 0.55, and all tokens are derived directly from the picked hue/sat/lit.
443
- * The optional palette argument is kept for API compatibility but ignored here,
444
- * so saved picks never get rewritten by a wallpaper-derived palette.
445
568
  */
446
- function genTokens(hue, sat, lit, _palette) {
569
+ function genTokens(hue, sat, lit) {
447
570
  const key = `${hue}|${sat}|${lit}`;
448
571
  if (tokensCacheKey === key && tokensCache) return tokensCache;
449
572
  tokensCacheKey = key;
@@ -1313,6 +1436,7 @@ window.__ModuleLoader__.load({
1313
1436
  //#endregion
1314
1437
  //#region src/client/wallpaper.ts
1315
1438
  let wpEl = null;
1439
+ let videoEl = null;
1316
1440
  let appliedTokenNames = [];
1317
1441
  let wpController = null;
1318
1442
  let snapshotListener = null;
@@ -1339,15 +1463,6 @@ window.__ModuleLoader__.load({
1339
1463
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1340
1464
  appliedTokenNames = [];
1341
1465
  }
1342
- /**
1343
- * Write the saved color's full token set as inline variables on body — the
1344
- * same write surface the theme presenter owns, but derived DIRECTLY from the
1345
- * saved pick, so the theme color never depends on the theme service's active
1346
- * state or the presenter's timing. The bg-base, sidebar and layer surfaces are
1347
- * re-emitted at their per-part alpha; every other token (labels, borders,
1348
- * brand) is written verbatim. No reads: nothing can observe a stale or reset
1349
- * theme value and leave the homepage on the system color.
1350
- */
1351
1466
  /** Label tokens flipped by the generated-background brightness verdict. */
1352
1467
  const LABEL_TOKENS = [
1353
1468
  "--dsw-alias-label-primary",
@@ -1380,36 +1495,42 @@ window.__ModuleLoader__.load({
1380
1495
  ensureTokenStyle().textContent = `body{${decls.join(";")}}`;
1381
1496
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1382
1497
  appliedTokenNames = Object.keys(tokens);
1383
- } catch (e) {}
1498
+ applyPartOpacities(ops);
1499
+ } catch {}
1384
1500
  }
1385
- const SETTINGS_STYLE_RULE = `[role="dialog"][aria-modal="true"][aria-labelledby]{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none)}`;
1501
+ const SETTINGS_PANEL_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
1502
+ const SETTINGS_STYLE_RULE = `${SETTINGS_PANEL_SEL}{background:var(--dsh-any-bg-settings-surface,var(--dsw-alias-bg-layer-2));backdrop-filter:var(--dsh-any-blur-settings,none);--dsw-alias-bg-layer-1:var(--dsh-any-bg-settings-layer-1);--dsw-alias-bg-layer-2:var(--dsh-any-bg-settings-layer-2);--dsw-alias-bg-layer-3:var(--dsh-any-bg-settings-layer-3)}${SETTINGS_PANEL_SEL} .dab-card{backdrop-filter:var(--dsh-any-blur-card-panels,none);-webkit-backdrop-filter:var(--dsh-any-blur-card-panels,none)}`;
1386
1503
  function applySettingsOverrides(op) {
1387
- if (op >= 1) {
1388
- document.documentElement.style.removeProperty("--dsh-any-bg-settings-surface");
1389
- return;
1390
- }
1391
- if (!rHasColor()) return;
1392
1504
  const [h, s, l] = rColor();
1393
- const layer2 = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-2"];
1505
+ const tokens = genTokens(h, s, l).tokens;
1506
+ const layer1 = tokens["--dsw-alias-bg-layer-1"];
1507
+ const layer2 = tokens["--dsw-alias-bg-layer-2"];
1508
+ const layer3 = tokens["--dsw-alias-bg-layer-3"];
1394
1509
  if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-surface", toRgba(layer2, op));
1510
+ if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-1", toRgba(layer1, op));
1511
+ if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-2", toRgba(layer2, op));
1512
+ if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-3", toRgba(layer3, op));
1395
1513
  }
1396
- /** Apply only the wallpaper layer (position, scale, opacity, blur) without
1397
- * touching the theme color palette. Used when generated background parameters
1398
- * change so the visual keeps updating without shifting the picked theme color. */
1399
- function applyWallpaper() {
1400
- applyWp();
1514
+ 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)}";
1515
+ function applyTrajectoryOverrides(op) {
1516
+ const [h, s, l] = rColor();
1517
+ const tokens = genTokens(h, s, l).tokens;
1518
+ const layer1 = tokens["--dsw-alias-bg-layer-1"];
1519
+ const layer2 = tokens["--dsw-alias-bg-layer-2"];
1520
+ const layer3 = tokens["--dsw-alias-bg-layer-3"];
1521
+ if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-1", toRgba(layer1, op));
1522
+ if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-2", toRgba(layer2, op));
1523
+ if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-3", toRgba(layer3, op));
1401
1524
  }
1402
- /** Apply the theme color. If the user has an explicit saved pick we use it
1403
- * directly; otherwise we fall back to extracting a dominant color from the
1404
- * current wallpaper (one HSL sample, no palette), so new uploads still get a
1405
- * matching theme automatically. */
1525
+ /** Apply the theme color: use the saved pick directly, or fall back to
1526
+ * extracting a dominant color from the current wallpaper. */
1406
1527
  function applyThemeColor() {
1407
1528
  if (rHasColor()) {
1408
1529
  applyWp();
1409
1530
  return;
1410
1531
  }
1411
1532
  const url = rWp();
1412
- if (url) extractWallpaperColor(url, rBgState()).then((hsl) => {
1533
+ if (url) extractWallpaperColor(url, cfg.backgroundType === "video" ? rVideoBgState() : rBgState()).then((hsl) => {
1413
1534
  if (hsl) cfg.color = hsl;
1414
1535
  applyWp();
1415
1536
  });
@@ -1426,6 +1547,13 @@ window.__ModuleLoader__.load({
1426
1547
  applyThemeColor();
1427
1548
  return;
1428
1549
  }
1550
+ if (type === "video") {
1551
+ clearDynamicBg();
1552
+ setBgDark(null);
1553
+ setWpUrl(null);
1554
+ applyThemeColor();
1555
+ return;
1556
+ }
1429
1557
  if (!cfg.generatedBg || cfg.generatedBg.type !== type) cfg.generatedBg = defaultParamsFor(type);
1430
1558
  applyGeneratedBg(cfg.generatedBg);
1431
1559
  }
@@ -1437,15 +1565,11 @@ window.__ModuleLoader__.load({
1437
1565
  function regenerateGeneratedBg() {
1438
1566
  const params = cfg.generatedBg;
1439
1567
  if (!params || cfg.backgroundType === "image") return;
1440
- const next = params.type === "mesh" ? {
1441
- ...params,
1442
- seed: randomSeed()
1443
- } : {
1568
+ cfg.generatedBg = {
1444
1569
  ...params,
1445
1570
  seed: randomSeed()
1446
1571
  };
1447
- cfg.generatedBg = next;
1448
- applyGeneratedBg(next);
1572
+ applyGeneratedBg(cfg.generatedBg);
1449
1573
  }
1450
1574
  /** Update a generated background's parameters and re-render. */
1451
1575
  function updateGeneratedBg(params) {
@@ -1455,6 +1579,7 @@ window.__ModuleLoader__.load({
1455
1579
  }
1456
1580
  function applyGeneratedBg(params) {
1457
1581
  clearDynamicBg();
1582
+ clearVideoEl();
1458
1583
  ensureWpContainer();
1459
1584
  wpController = createDynamicBackground(params);
1460
1585
  if (wpEl) {
@@ -1466,7 +1591,7 @@ window.__ModuleLoader__.load({
1466
1591
  const controller = wpController;
1467
1592
  const frame = controller.snapshot();
1468
1593
  setWpUrl(frame);
1469
- applyWallpaper();
1594
+ applyWp();
1470
1595
  snapshotListener?.();
1471
1596
  setBgDark(null);
1472
1597
  analyzeFrameDark(frame).then((dark) => {
@@ -1532,14 +1657,38 @@ window.__ModuleLoader__.load({
1532
1657
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-settings", `blur(${px}px)`);
1533
1658
  else document.documentElement.style.removeProperty("--dsh-any-blur-settings");
1534
1659
  }
1660
+ /** Apply the main-background opacity to the center/details columns instead of
1661
+ * the frame. The frame's translucent bg-base sits UNDER the sidebar, so
1662
+ * reducing the main-bg opacity stacked a second alpha onto the sidebar; moving
1663
+ * the alpha onto the columns keeps the sidebar owned by its own slider. */
1664
+ function applyPartOpacities(ops) {
1665
+ if (!(rHasColor() || rBgDark() !== null)) return;
1666
+ discoverParts();
1667
+ if (frameEl === null) return;
1668
+ const [h, s, l] = rColor();
1669
+ const base = genTokens(h, s, l).tokens["--dsw-alias-bg-base"];
1670
+ frameEl.style.background = "transparent";
1671
+ if (centerEl !== null) centerEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
1672
+ if (detailsEl !== null) detailsEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
1673
+ }
1674
+ /** Blur of the option panels inside the settings dialog (.dab-card), owned by
1675
+ * the "dialog option panel" (card) blur slider. Written as a plugin-owned
1676
+ * variable consumed by SETTINGS_STYLE_RULE — deliberately NOT applied to the
1677
+ * homepage center/details columns, which this slider must never touch. */
1678
+ function applyCardPanelsBlur(px) {
1679
+ if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-card-panels", `blur(${px}px)`);
1680
+ else document.documentElement.style.removeProperty("--dsh-any-blur-card-panels");
1681
+ }
1535
1682
  /** Apply per-part interface blur to the AppFrame columns + settings panel. */
1536
1683
  function applyPartBlurs(blurs) {
1537
1684
  discoverParts();
1538
- setBlur(frameEl, blurs.bg);
1685
+ setBlur(frameEl, 0);
1539
1686
  setBlur(sidebarEl, blurs.sidebar);
1540
- setBlur(centerEl, blurs.card);
1541
- setBlur(detailsEl, blurs.card);
1687
+ setBlur(centerEl, blurs.bg);
1688
+ setBlur(detailsEl, blurs.bg);
1689
+ applyCardPanelsBlur(blurs.card);
1542
1690
  applySettingsBlur(blurs.settings);
1691
+ applyViewCards();
1543
1692
  }
1544
1693
  /** Live per-part blur update during slider drag (no full re-apply). */
1545
1694
  function setPartBlur(part, v) {
@@ -1547,13 +1696,178 @@ window.__ModuleLoader__.load({
1547
1696
  applySettingsBlur(v);
1548
1697
  return;
1549
1698
  }
1699
+ if (part === "card") {
1700
+ applyCardPanelsBlur(v);
1701
+ return;
1702
+ }
1703
+ if (part === "chat" || part === "trajectory") {
1704
+ applyViewCards();
1705
+ return;
1706
+ }
1550
1707
  discoverParts();
1551
- if (part === "bg") setBlur(frameEl, v);
1552
- else if (part === "sidebar") setBlur(sidebarEl, v);
1553
- else {
1708
+ if (part === "bg") {
1554
1709
  setBlur(centerEl, v);
1555
1710
  setBlur(detailsEl, v);
1711
+ } else setBlur(sidebarEl, v);
1712
+ }
1713
+ const VIEW_CARDS = [{
1714
+ sel: "[data-chat-flow]",
1715
+ mark: "data-dab-chat-card",
1716
+ prev: "dabChatPrev",
1717
+ opacity: rChatTextOpacity,
1718
+ blur: () => rBlurs().chat,
1719
+ fallback: true
1720
+ }, {
1721
+ sel: "[data-conversation-composer-overlay]",
1722
+ mark: "data-dab-traj-card",
1723
+ prev: "dabTrajPrev",
1724
+ opacity: rTrajectoryOpacity,
1725
+ blur: () => rBlurs().trajectory,
1726
+ plain: true
1727
+ }];
1728
+ const viewTargets = VIEW_CARDS.map(() => null);
1729
+ function isScrollableY(el) {
1730
+ const oy = getComputedStyle(el).overflowY;
1731
+ return oy === "auto" || oy === "scroll" || oy === "overlay";
1732
+ }
1733
+ /** Whether the subtree hosts the chat input (textarea / contenteditable /
1734
+ * textbox role) — used to keep the card off the input row. */
1735
+ function containsChatEditor(el) {
1736
+ return el.querySelector("textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[contenteditable=\"plaintext-only\"],[role=\"textbox\"]") !== null;
1737
+ }
1738
+ /** Walk down from a coarse candidate toward the actual message column: stop
1739
+ * at a scroll container (the card surface must stay pinned to the scroll
1740
+ * port); while the chat input lives inside, descend into the tallest child that
1741
+ * does NOT contain it (the header row is short, the input row holds the
1742
+ * editor); otherwise peel wrappers dominated (>= 85%) by a single child so
1743
+ * tab bars / titles stay outside the card. */
1744
+ function refineMessageColumn(start) {
1745
+ let cur = start;
1746
+ for (let depth = 0; depth < 10; depth++) {
1747
+ if (isScrollableY(cur)) break;
1748
+ const kids = Array.from(cur.children).filter((k) => k instanceof HTMLElement);
1749
+ if (kids.length === 0) break;
1750
+ const tallest = kids.reduce((a, b) => b.clientHeight > a.clientHeight ? b : a);
1751
+ if (containsChatEditor(cur)) {
1752
+ const candidates = kids.filter((k) => !containsChatEditor(k) && k.clientHeight >= cur.clientHeight * .4);
1753
+ if (candidates.length === 0) break;
1754
+ cur = candidates.reduce((a, b) => b.clientHeight > a.clientHeight ? b : a);
1755
+ continue;
1756
+ }
1757
+ if (kids.length > 1 && tallest.clientHeight >= cur.clientHeight * .85) {
1758
+ cur = tallest;
1759
+ continue;
1760
+ }
1761
+ break;
1762
+ }
1763
+ return cur;
1764
+ }
1765
+ function discoverViewTarget(idx, spec) {
1766
+ if (centerEl === null || !document.body.contains(centerEl)) {
1767
+ viewTargets[idx] = null;
1768
+ return null;
1769
+ }
1770
+ const marked = centerEl.querySelector(spec.sel);
1771
+ const cached = viewTargets[idx];
1772
+ if (marked !== null) {
1773
+ if (cached !== null && cached !== marked) {
1774
+ setBlur(cached, 0);
1775
+ restoreCardHost(cached, spec.mark, spec.prev, spec.plain === true);
1776
+ }
1777
+ viewTargets[idx] = marked;
1778
+ return marked;
1556
1779
  }
1780
+ if (cached !== null && centerEl.contains(cached)) return cached;
1781
+ viewTargets[idx] = null;
1782
+ if (spec.fallback !== true) return null;
1783
+ if (centerEl.querySelector("[data-conversation-scroll]") !== null) return null;
1784
+ if (spec.opacity() <= 0 && spec.blur() <= 0) return null;
1785
+ let best = null;
1786
+ let bestArea = 0;
1787
+ for (const el of Array.from(centerEl.querySelectorAll("*"))) {
1788
+ if (!isScrollableY(el)) continue;
1789
+ if (el.clientHeight < centerEl.clientHeight * .35) continue;
1790
+ const area = el.clientWidth * el.clientHeight;
1791
+ if (area > bestArea) {
1792
+ bestArea = area;
1793
+ best = el;
1794
+ }
1795
+ }
1796
+ if (best === null) for (const el of Array.from(centerEl.children)) {
1797
+ if (!(el instanceof HTMLElement)) continue;
1798
+ if (el.clientHeight < centerEl.clientHeight * .5) continue;
1799
+ if (el.clientHeight > (best?.clientHeight ?? 0)) best = el;
1800
+ }
1801
+ const refined = best !== null ? refineMessageColumn(best) : null;
1802
+ viewTargets[idx] = refined;
1803
+ return refined;
1804
+ }
1805
+ /** Stash the host's own inline values so teardown restores them exactly.
1806
+ * Plain views only get a background override, so only that is stashed. */
1807
+ function stashCardPrev(el, prev, plain) {
1808
+ const ds = el.dataset;
1809
+ ds[prev + "Bg"] = el.style.getPropertyValue("background");
1810
+ if (plain) return;
1811
+ ds[prev + "Border"] = el.style.getPropertyValue("border");
1812
+ ds[prev + "Radius"] = el.style.getPropertyValue("border-radius");
1813
+ ds[prev + "Padding"] = el.style.getPropertyValue("padding");
1814
+ }
1815
+ /** Undo the inline styling, restoring the host's previous inline values. */
1816
+ function restoreCardHost(el, mark, prev, plain) {
1817
+ if (!el.hasAttribute(mark)) return;
1818
+ const ds = el.dataset;
1819
+ const restore = (prop, v) => {
1820
+ if (v !== void 0 && v !== "") el.style.setProperty(prop, v);
1821
+ else el.style.removeProperty(prop);
1822
+ };
1823
+ restore("background", ds[prev + "Bg"]);
1824
+ if (!plain) {
1825
+ restore("border", ds[prev + "Border"]);
1826
+ restore("border-radius", ds[prev + "Radius"]);
1827
+ restore("padding", ds[prev + "Padding"]);
1828
+ delete ds[prev + "Border"];
1829
+ delete ds[prev + "Radius"];
1830
+ delete ds[prev + "Padding"];
1831
+ }
1832
+ delete ds[prev + "Bg"];
1833
+ el.removeAttribute(mark);
1834
+ }
1835
+ /** Teardown only: strip every view treatment and hand the hosts back untouched. */
1836
+ function removeViewCards() {
1837
+ VIEW_CARDS.forEach((spec, i) => {
1838
+ const el = viewTargets[i];
1839
+ if (el !== null) {
1840
+ setBlur(el, 0);
1841
+ restoreCardHost(el, spec.mark, spec.prev, spec.plain === true);
1842
+ }
1843
+ viewTargets[i] = null;
1844
+ });
1845
+ }
1846
+ /** Re-derive the conversation view cards from the current config. Cheap
1847
+ * enough for live slider drags; the card structure is applied unconditionally
1848
+ * once the host exists so the layout never reflows when a slider leaves zero. */
1849
+ function applyViewCards() {
1850
+ discoverParts();
1851
+ if (centerEl === null) return;
1852
+ const [h, s, l] = rColor();
1853
+ const surface = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-1"];
1854
+ VIEW_CARDS.forEach((spec, i) => {
1855
+ const target = discoverViewTarget(i, spec);
1856
+ if (target === null) return;
1857
+ const plain = spec.plain === true;
1858
+ const opacity = spec.opacity();
1859
+ const blurPx = spec.blur();
1860
+ if (!plain) {
1861
+ if (!target.hasAttribute(spec.mark)) stashCardPrev(target, spec.prev, false);
1862
+ const borderAlpha = opacity > 0 ? Math.min(1, opacity * 1.5) : blurPx > 0 ? .35 : 0;
1863
+ target.style.background = surface !== void 0 ? toRgba(surface, opacity) : "transparent";
1864
+ target.style.border = surface !== void 0 ? `1px solid ${toRgba(surface, borderAlpha)}` : "1px solid transparent";
1865
+ target.style.borderRadius = "16px";
1866
+ target.style.padding = "18px";
1867
+ }
1868
+ target.setAttribute(spec.mark, "1");
1869
+ setBlur(target, blurPx);
1870
+ });
1557
1871
  }
1558
1872
  let partsObserver = null;
1559
1873
  /** Watch for the AppFrame mounting so persisted blurs land even when the shell
@@ -1562,8 +1876,9 @@ window.__ModuleLoader__.load({
1562
1876
  function watchParts() {
1563
1877
  if (partsObserver !== null || typeof MutationObserver === "undefined") return;
1564
1878
  partsObserver = new MutationObserver(() => {
1565
- if (frameEl !== null && sidebarEl !== null && centerEl !== null && detailsEl !== null && document.body.contains(frameEl)) return;
1879
+ if (frameEl !== null && sidebarEl !== null && centerEl !== null && detailsEl !== null && document.body.contains(frameEl) && viewTargets.every((el) => el !== null && document.body.contains(el))) return;
1566
1880
  applyPartBlurs(rBlurs());
1881
+ applyPartOpacities(rOps());
1567
1882
  });
1568
1883
  partsObserver.observe(document.body, {
1569
1884
  childList: true,
@@ -1581,24 +1896,111 @@ window.__ModuleLoader__.load({
1581
1896
  document.body.prepend(wpEl);
1582
1897
  }
1583
1898
  }
1899
+ function clearVideoEl() {
1900
+ if (videoEl === null) return;
1901
+ videoEl.pause();
1902
+ videoEl.removeAttribute("src");
1903
+ videoEl.load();
1904
+ videoEl.remove();
1905
+ videoEl = null;
1906
+ }
1907
+ /** Intrinsic-size cache for the center mode (native pixels of the current image). */
1908
+ let imgNat = null;
1909
+ function imageNatSize(url, cb) {
1910
+ if (imgNat !== null && imgNat.url === url) {
1911
+ cb(imgNat.w, imgNat.h);
1912
+ return;
1913
+ }
1914
+ const img = new Image();
1915
+ img.onload = () => {
1916
+ imgNat = {
1917
+ url,
1918
+ w: img.naturalWidth,
1919
+ h: img.naturalHeight
1920
+ };
1921
+ cb(img.naturalWidth, img.naturalHeight);
1922
+ };
1923
+ img.onerror = () => cb(0, 0);
1924
+ img.src = url;
1925
+ }
1584
1926
  function applyImageWp(url) {
1585
1927
  clearDynamicBg();
1928
+ clearVideoEl();
1586
1929
  ensureWpContainer();
1587
1930
  const bg = rBgState();
1931
+ const mode = rBgMode();
1588
1932
  const next = `url("${url}")`;
1589
- if (wpEl.style.backgroundImage !== next) {
1590
- wpEl.style.backgroundImage = next;
1933
+ if (wpEl.style.backgroundImage !== next) wpEl.style.backgroundImage = next;
1934
+ if (mode === "fit") {
1935
+ wpEl.style.backgroundRepeat = "no-repeat";
1936
+ if (bg.iw > 0) {
1937
+ const fit = Math.min(window.innerWidth / bg.iw, window.innerHeight / bg.ih);
1938
+ const w = bg.iw * fit * bg.zoom;
1939
+ const h = bg.ih * fit * bg.zoom;
1940
+ wpEl.style.backgroundSize = `${w}px ${h}px`;
1941
+ wpEl.style.backgroundPosition = `${bg.x * window.innerWidth - w / 2}px ${bg.y * window.innerHeight - h / 2}px`;
1942
+ } else {
1943
+ wpEl.style.backgroundSize = "contain";
1944
+ wpEl.style.backgroundPosition = "center";
1945
+ }
1946
+ } else if (mode === "fill") {
1591
1947
  wpEl.style.backgroundRepeat = "no-repeat";
1948
+ wpEl.style.backgroundSize = "cover";
1949
+ wpEl.style.backgroundPosition = "center";
1950
+ } else if (mode === "stretch") {
1951
+ wpEl.style.backgroundRepeat = "no-repeat";
1952
+ wpEl.style.backgroundSize = "100% 100%";
1953
+ wpEl.style.backgroundPosition = "center";
1954
+ } else if (mode === "tile") {
1955
+ wpEl.style.backgroundRepeat = "repeat";
1956
+ wpEl.style.backgroundSize = "auto";
1957
+ wpEl.style.backgroundPosition = "0px 0px";
1958
+ } else {
1959
+ wpEl.style.backgroundRepeat = "no-repeat";
1960
+ wpEl.style.backgroundSize = "contain";
1961
+ wpEl.style.backgroundPosition = "center";
1962
+ imageNatSize(url, (w, h) => {
1963
+ if (!wpEl || wpEl.style.backgroundImage !== next || rBgMode() !== "center") return;
1964
+ if (w > 0 && h > 0) {
1965
+ wpEl.style.backgroundSize = `${w}px ${h}px`;
1966
+ wpEl.style.backgroundPosition = "center";
1967
+ }
1968
+ });
1592
1969
  }
1593
- if (bg.iw > 0) {
1970
+ applyWpEffects();
1971
+ }
1972
+ /** Video wallpaper: a muted looping <video> inside the wallpaper layer.
1973
+ * Placement modes map onto object-fit (tile has no video equivalent and
1974
+ * falls back to cover). */
1975
+ function applyVideoWp(url) {
1976
+ clearDynamicBg();
1977
+ ensureWpContainer();
1978
+ if (wpEl.style.backgroundImage !== "none") wpEl.style.backgroundImage = "none";
1979
+ if (videoEl === null || !videoEl.isConnected) {
1980
+ videoEl = document.createElement("video");
1981
+ videoEl.muted = true;
1982
+ videoEl.loop = true;
1983
+ videoEl.autoplay = true;
1984
+ videoEl.playsInline = true;
1985
+ videoEl.setAttribute("playsinline", "");
1986
+ videoEl.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-position:center;";
1987
+ videoEl.setAttribute("src", url);
1988
+ videoEl.play().catch(() => void 0);
1989
+ wpEl.appendChild(videoEl);
1990
+ } else if (videoEl.getAttribute("src") !== url) {
1991
+ videoEl.setAttribute("src", url);
1992
+ videoEl.play().catch(() => void 0);
1993
+ }
1994
+ const mode = rBgMode();
1995
+ const bg = rVideoBgState();
1996
+ if (mode === "fit" && bg.iw > 0) {
1594
1997
  const fit = Math.min(window.innerWidth / bg.iw, window.innerHeight / bg.ih);
1595
1998
  const w = bg.iw * fit * bg.zoom;
1596
1999
  const h = bg.ih * fit * bg.zoom;
1597
- wpEl.style.backgroundSize = `${w}px ${h}px`;
1598
- wpEl.style.backgroundPosition = `${bg.x * window.innerWidth - w / 2}px ${bg.y * window.innerHeight - h / 2}px`;
2000
+ videoEl.style.cssText = `position:absolute;left:${bg.x * window.innerWidth - w / 2}px;top:${bg.y * window.innerHeight - h / 2}px;width:${w}px;height:${h}px;object-fit:fill;`;
1599
2001
  } else {
1600
- wpEl.style.backgroundSize = "contain";
1601
- wpEl.style.backgroundPosition = "center";
2002
+ videoEl.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-position:center;";
2003
+ videoEl.style.objectFit = mode === "stretch" ? "fill" : mode === "fill" || mode === "tile" ? "cover" : "contain";
1602
2004
  }
1603
2005
  applyWpEffects();
1604
2006
  }
@@ -1610,7 +2012,17 @@ window.__ModuleLoader__.load({
1610
2012
  }
1611
2013
  function applyWp() {
1612
2014
  const url = rWp();
1613
- if (cfg.backgroundType !== "image" && cfg.generatedBg) {
2015
+ if (cfg.backgroundType === "video") {
2016
+ const vurl = rWpVideo();
2017
+ if (vurl) applyVideoWp(vurl);
2018
+ else {
2019
+ clearDynamicBg();
2020
+ clearVideoEl();
2021
+ wpEl?.remove();
2022
+ wpEl = null;
2023
+ }
2024
+ } else if (cfg.backgroundType !== "image" && cfg.generatedBg) {
2025
+ clearVideoEl();
1614
2026
  if (!wpController) {
1615
2027
  applyGeneratedBg(cfg.generatedBg);
1616
2028
  return;
@@ -1621,27 +2033,44 @@ window.__ModuleLoader__.load({
1621
2033
  } else if (url) applyImageWp(url);
1622
2034
  else {
1623
2035
  clearDynamicBg();
2036
+ clearVideoEl();
1624
2037
  wpEl?.remove();
1625
2038
  wpEl = null;
1626
2039
  }
1627
2040
  if (rHasColor() || rBgDark() !== null) applyCustomTokens(rOps());
1628
- if (rHasColor()) applySettingsOverrides(rSop());
2041
+ if (rHasColor()) {
2042
+ applySettingsOverrides(rSop());
2043
+ applyTrajectoryOverrides(rTrajectoryOpacity());
2044
+ }
1629
2045
  applyPartBlurs(rBlurs());
1630
2046
  }
1631
2047
  function teardownWp() {
1632
2048
  clearDynamicBg();
2049
+ clearVideoEl();
1633
2050
  setBgDark(null);
1634
2051
  wpEl?.remove();
1635
2052
  wpEl = null;
1636
2053
  clearCustomTokens();
1637
2054
  tokenStyleEl?.remove();
1638
2055
  tokenStyleEl = null;
2056
+ removeViewCards();
1639
2057
  document.documentElement.style.removeProperty("--dsh-any-bg-settings-surface");
2058
+ document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-1");
2059
+ document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-2");
2060
+ document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-3");
2061
+ document.documentElement.style.removeProperty("--dsh-any-traj-layer-1");
2062
+ document.documentElement.style.removeProperty("--dsh-any-traj-layer-2");
2063
+ document.documentElement.style.removeProperty("--dsh-any-traj-layer-3");
2064
+ document.documentElement.style.removeProperty("--dsh-any-bg-settings-card-surface");
1640
2065
  document.documentElement.style.removeProperty("--dsh-any-blur-settings");
2066
+ document.documentElement.style.removeProperty("--dsh-any-blur-card-panels");
1641
2067
  setBlur(frameEl, 0);
1642
2068
  setBlur(sidebarEl, 0);
1643
2069
  setBlur(centerEl, 0);
1644
2070
  setBlur(detailsEl, 0);
2071
+ if (frameEl !== null) frameEl.style.removeProperty("background");
2072
+ if (centerEl !== null) centerEl.style.removeProperty("background");
2073
+ if (detailsEl !== null) detailsEl.style.removeProperty("background");
1645
2074
  stopWatchingParts();
1646
2075
  }
1647
2076
  /** Live wallpaper-opacity updates during slider drag (no full re-apply). */
@@ -1652,6 +2081,69 @@ window.__ModuleLoader__.load({
1652
2081
  function setWpBlur(v) {
1653
2082
  if (wpEl) wpEl.style.filter = v > 0 ? `blur(${v}px)` : "none";
1654
2083
  }
2084
+ //#endregion
2085
+ //#region src/client/utils/video.ts
2086
+ /**
2087
+ * Video-background helpers: capturing a single frame as a JPEG snapshot.
2088
+ * The snapshot stands in for everything that only understands still images:
2089
+ * the settings preview, theme-color extraction and the eyedropper. Uploads
2090
+ * stream the raw file over the binary upload route (see rpc.uploadVideo) —
2091
+ * a data-URL detour would inflate the bytes and trip the RPC body limit.
2092
+ */
2093
+ function grabFrame(v) {
2094
+ const w = v.videoWidth, h = v.videoHeight;
2095
+ if (w <= 0 || h <= 0) return null;
2096
+ const canvas = document.createElement("canvas");
2097
+ canvas.width = w;
2098
+ canvas.height = h;
2099
+ const ctx = canvas.getContext("2d");
2100
+ if (!ctx) return null;
2101
+ ctx.drawImage(v, 0, 0, w, h);
2102
+ try {
2103
+ return canvas.toDataURL("image/jpeg", .85);
2104
+ } catch {
2105
+ return null;
2106
+ }
2107
+ }
2108
+ /**
2109
+ * Capture one representative frame from a video data URL as a JPEG data URL.
2110
+ * Seeks to ~10% of the duration (first frame is often black) and waits for
2111
+ * the seek to land; resolves null when the video cannot be decoded. A timeout
2112
+ * keeps a broken file from hanging the upload flow.
2113
+ */
2114
+ function captureVideoSnapshot(url) {
2115
+ return new Promise((resolve) => {
2116
+ const v = document.createElement("video");
2117
+ let settled = false;
2118
+ const done = (r) => {
2119
+ if (settled) return;
2120
+ settled = true;
2121
+ window.clearTimeout(timer);
2122
+ v.removeAttribute("src");
2123
+ v.load();
2124
+ resolve(r);
2125
+ };
2126
+ const timer = window.setTimeout(() => done(grabFrame(v)), 6e3);
2127
+ v.muted = true;
2128
+ v.preload = "auto";
2129
+ v.playsInline = true;
2130
+ v.onerror = () => done(null);
2131
+ v.onloadeddata = () => {
2132
+ const target = isFinite(v.duration) && v.duration > .6 ? Math.min(v.duration * .1, 2) : 0;
2133
+ if (target <= 0) {
2134
+ done(grabFrame(v));
2135
+ return;
2136
+ }
2137
+ v.onseeked = () => done(grabFrame(v));
2138
+ try {
2139
+ v.currentTime = target;
2140
+ } catch {
2141
+ done(grabFrame(v));
2142
+ }
2143
+ };
2144
+ v.src = url;
2145
+ });
2146
+ }
1655
2147
  const UI_CSS = `
1656
2148
  /* The section is rendered INLINE inside the host settings dialog's content
1657
2149
  * column: the host provides the modal chrome (backdrop, centering, closing).
@@ -1934,6 +2426,44 @@ window.__ModuleLoader__.load({
1934
2426
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 10.4l-2.8-2.8-4.8 4.8" })
1935
2427
  ]
1936
2428
  });
2429
+ const VideoIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2430
+ size,
2431
+ className,
2432
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
2433
+ x: "1.5",
2434
+ y: "4.2",
2435
+ width: "8.8",
2436
+ height: "7.6",
2437
+ rx: "2"
2438
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10.3 6.9l4.2-2.4v7l-4.2-2.4" })]
2439
+ });
2440
+ const TextIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
2441
+ size,
2442
+ className,
2443
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 4.2h10M3 8h10M3 11.8h6.5" })
2444
+ });
2445
+ const TrajectoryIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2446
+ size,
2447
+ className,
2448
+ children: [
2449
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 3.2h11M2.5 6.6h11M2.5 10h11" }),
2450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2451
+ cx: "4.4",
2452
+ cy: "3.2",
2453
+ r: "1.1"
2454
+ }),
2455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2456
+ cx: "8",
2457
+ cy: "6.6",
2458
+ r: "1.1"
2459
+ }),
2460
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2461
+ cx: "11.4",
2462
+ cy: "10",
2463
+ r: "1.1"
2464
+ })
2465
+ ]
2466
+ });
1937
2467
  const SlidersIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1938
2468
  size,
1939
2469
  className,
@@ -2893,7 +3423,7 @@ window.__ModuleLoader__.load({
2893
3423
  paint(inputRef.current, v);
2894
3424
  }
2895
3425
  if (valRef.current) valRef.current.textContent = fmt(v);
2896
- onInput(v);
3426
+ onInput?.(v);
2897
3427
  onChange(v);
2898
3428
  };
2899
3429
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2922,7 +3452,7 @@ window.__ModuleLoader__.load({
2922
3452
  const el = e.target;
2923
3453
  const v = Number(el.value);
2924
3454
  paint(el, v);
2925
- onInput(v);
3455
+ onInput?.(v);
2926
3456
  if (valRef.current) valRef.current.textContent = fmt(v);
2927
3457
  },
2928
3458
  onChange: (e) => onChange(Number(e.target.value))
@@ -2956,6 +3486,16 @@ window.__ModuleLoader__.load({
2956
3486
  isSettings: true,
2957
3487
  labelKey: "uiSop",
2958
3488
  Icon: GearIcon
3489
+ },
3490
+ {
3491
+ isChat: true,
3492
+ labelKey: "uiChatRegion",
3493
+ Icon: TextIcon
3494
+ },
3495
+ {
3496
+ isTrajectory: true,
3497
+ labelKey: "uiTrajectory",
3498
+ Icon: TrajectoryIcon
2959
3499
  }
2960
3500
  ];
2961
3501
  function InterfacePage({ p }) {
@@ -2980,10 +3520,10 @@ window.__ModuleLoader__.load({
2980
3520
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2981
3521
  className: "dab-grid-parts",
2982
3522
  children: PARTS.map((part, i) => {
2983
- const { labelKey, Icon, isSettings } = part;
3523
+ const { labelKey, Icon, isSettings, isChat, isTrajectory } = part;
2984
3524
  const opKey = part.opKey;
2985
- const blurKey = opKey ?? "settings";
2986
- const opacity = isSettings ? rSop() : rOps()[opKey];
3525
+ const blurKey = isChat ? "chat" : isTrajectory ? "trajectory" : isSettings ? "settings" : opKey;
3526
+ const opacity = isChat ? rChatTextOpacity() : isTrajectory ? rTrajectoryOpacity() : isSettings ? rSop() : rOps()[opKey];
2987
3527
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2988
3528
  className: "dab-card dab-card-hover dab-rise",
2989
3529
  style: { "--d": i + 1 },
@@ -3014,7 +3554,13 @@ window.__ModuleLoader__.load({
3014
3554
  fmt: (v) => `${v}%`,
3015
3555
  onInput: (v) => {
3016
3556
  const op = v / 100;
3017
- if (isSettings) {
3557
+ if (isChat) {
3558
+ cfg.chatTextOpacity = op;
3559
+ applyViewCards();
3560
+ } else if (isTrajectory) {
3561
+ cfg.trajectoryOpacity = op;
3562
+ applyTrajectoryOverrides(op);
3563
+ } else if (isSettings) {
3018
3564
  cfg.settingsOpacity = op;
3019
3565
  applySettingsOverrides(op);
3020
3566
  } else {
@@ -3027,7 +3573,15 @@ window.__ModuleLoader__.load({
3027
3573
  },
3028
3574
  onChange: (v) => {
3029
3575
  const op = v / 100;
3030
- if (isSettings) setSop(op);
3576
+ if (isChat) {
3577
+ cfg.chatTextOpacity = op;
3578
+ applyViewCards();
3579
+ saveConfig();
3580
+ } else if (isTrajectory) {
3581
+ cfg.trajectoryOpacity = op;
3582
+ applyTrajectoryOverrides(op);
3583
+ saveConfig();
3584
+ } else if (isSettings) setSop(op);
3031
3585
  else {
3032
3586
  const ops = { ...rOps() };
3033
3587
  ops[opKey] = op;
@@ -3076,10 +3630,13 @@ window.__ModuleLoader__.load({
3076
3630
  }
3077
3631
  //#endregion
3078
3632
  //#region src/client/components/BgEditor.tsx
3633
+ /** Placement slot for the active background type: video framing lives in its
3634
+ * own state so image and video edits never overwrite each other. */
3635
+ const activeState = () => cfg.backgroundType === "video" ? cfg.videoBgState : cfg.bgState;
3079
3636
  function BgEditor({ url, t, onClose, onCommit }) {
3080
3637
  const pw = Math.min(window.innerWidth * .75, 860);
3081
3638
  const ph = Math.round(pw * window.innerHeight / window.innerWidth);
3082
- const saved = rBgState();
3639
+ const saved = activeState();
3083
3640
  const [zoom, setZoom] = (0, react.useState)(saved.iw > 0 ? saved.zoom : 1);
3084
3641
  const [pos, setPos] = (0, react.useState)(saved.iw > 0 ? {
3085
3642
  x: saved.x * pw,
@@ -3110,7 +3667,7 @@ window.__ModuleLoader__.load({
3110
3667
  w,
3111
3668
  h
3112
3669
  });
3113
- const s = rBgState();
3670
+ const s = activeState();
3114
3671
  if (s.iw > 0 && s.iw === img.width && s.ih === img.height) {
3115
3672
  setZoom(s.zoom);
3116
3673
  setPos({
@@ -3264,8 +3821,30 @@ window.__ModuleLoader__.load({
3264
3821
  //#endregion
3265
3822
  //#region src/client/components/pages/BackgroundPage.tsx
3266
3823
  const SEG_W = 108;
3824
+ const BG_MODES = [
3825
+ {
3826
+ mode: "fit",
3827
+ labelKey: "bgModeFit"
3828
+ },
3829
+ {
3830
+ mode: "fill",
3831
+ labelKey: "bgModeFill"
3832
+ },
3833
+ {
3834
+ mode: "stretch",
3835
+ labelKey: "bgModeStretch"
3836
+ },
3837
+ {
3838
+ mode: "tile",
3839
+ labelKey: "bgModeTile"
3840
+ },
3841
+ {
3842
+ mode: "center",
3843
+ labelKey: "bgModeCenter"
3844
+ }
3845
+ ];
3267
3846
  function BackgroundPage({ p }) {
3268
- const { t, setWp, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, useStore } = p;
3847
+ const { t, setWp, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, useStore } = p;
3269
3848
  const store = useStore((s) => s);
3270
3849
  const storeUrl = store.url;
3271
3850
  const backgroundType = store.backgroundType;
@@ -3275,9 +3854,16 @@ window.__ModuleLoader__.load({
3275
3854
  const [editorOpen, setEditorOpen] = (0, react.useState)(false);
3276
3855
  const [dragOver, setDragOver] = (0, react.useState)(false);
3277
3856
  const [spinTick, setSpinTick] = (0, react.useState)(0);
3278
- const isGenerated = backgroundType !== "image";
3857
+ const [mode, setModeState] = (0, react.useState)(rBgMode());
3858
+ const isVideo = backgroundType === "video";
3859
+ const isStatic = backgroundType === "image" || isVideo;
3860
+ const isGenerated = !isStatic;
3279
3861
  const activeGenType = isGenerated && generatedBg ? generatedBg.type : "mesh";
3280
3862
  const onFileSelect = (f) => {
3863
+ if (f.type.startsWith("video/")) {
3864
+ setVideo(f, f.type);
3865
+ return;
3866
+ }
3281
3867
  readImg(f, (d) => {
3282
3868
  if (d) setWp(d);
3283
3869
  });
@@ -3286,10 +3872,18 @@ window.__ModuleLoader__.load({
3286
3872
  e.preventDefault();
3287
3873
  setDragOver(false);
3288
3874
  const f = e.dataTransfer.files?.[0];
3289
- if (f && f.type.startsWith("image/")) onFileSelect(f);
3875
+ if (f && (f.type.startsWith("image/") || f.type.startsWith("video/"))) onFileSelect(f);
3290
3876
  };
3291
- const switchToImage = () => {
3292
- if (backgroundType !== "image") setBgType("image");
3877
+ const switchToStatic = () => {
3878
+ if (isStatic) return;
3879
+ setBgType(isVideo || cfg.videoMime !== null ? "video" : "image");
3880
+ };
3881
+ const setMode = (m) => {
3882
+ if (m === mode) return;
3883
+ setModeState(m);
3884
+ cfg.bgMode = m;
3885
+ applyWp();
3886
+ saveConfig();
3293
3887
  };
3294
3888
  const setGenType = (type) => {
3295
3889
  if (type === activeGenType) return;
@@ -3367,15 +3961,20 @@ window.__ModuleLoader__.load({
3367
3961
  isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3368
3962
  className: "dab-hero-badge",
3369
3963
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 11 }), t("liveBadge")]
3964
+ }) : isVideo ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3965
+ className: "dab-hero-badge",
3966
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(VideoIcon, { size: 11 }), t("bgVideoBadge")]
3370
3967
  }) : null,
3371
3968
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3372
3969
  className: "dab-hero-veil",
3373
- children: [backgroundType === "image" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3970
+ children: [isStatic && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3374
3971
  type: "button",
3375
3972
  className: "dab-btn",
3973
+ disabled: mode !== "fit",
3974
+ title: mode !== "fit" ? t("bgEditLocked") : void 0,
3376
3975
  onClick: () => setEditorOpen(true),
3377
3976
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 13 }), t("bgEdit")]
3378
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3977
+ }) : isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3379
3978
  type: "button",
3380
3979
  className: "dab-btn",
3381
3980
  onClick: () => {
@@ -3383,7 +3982,7 @@ window.__ModuleLoader__.load({
3383
3982
  setSpinTick((x) => x + 1);
3384
3983
  },
3385
3984
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 13 }), t("bgRegenerate")]
3386
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3985
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3387
3986
  type: "button",
3388
3987
  className: "dab-btn dab-btn-danger",
3389
3988
  onClick: () => setWp(null),
@@ -3418,7 +4017,7 @@ window.__ModuleLoader__.load({
3418
4017
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3419
4018
  type: "button",
3420
4019
  className: `dab-seg-item${!isGenerated ? " is-active" : ""}`,
3421
- onClick: switchToImage,
4020
+ onClick: switchToStatic,
3422
4021
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhotoIcon, { size: 14 }), t("bgSourceImage")]
3423
4022
  }),
3424
4023
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -3432,28 +4031,44 @@ window.__ModuleLoader__.load({
3432
4031
  ]
3433
4032
  })
3434
4033
  }),
3435
- !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
4034
+ !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3436
4035
  className: "dab-card dab-card-hover dab-rise",
3437
4036
  style: { "--d": 3 },
3438
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4037
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3439
4038
  className: "dab-chip-row",
3440
4039
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3441
4040
  type: "button",
3442
4041
  className: "dab-btn dab-btn-primary",
3443
4042
  onClick: () => fileRef.current?.click(),
3444
4043
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UploadIcon, { size: 14 }), t("bgChoose")]
3445
- }), storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4044
+ }), storeUrl || isVideo ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [isStatic && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3446
4045
  type: "button",
3447
4046
  className: "dab-btn",
4047
+ disabled: mode !== "fit",
4048
+ title: mode !== "fit" ? t("bgEditLocked") : void 0,
3448
4049
  onClick: () => setEditorOpen(true),
3449
4050
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 14 }), t("bgEdit")]
3450
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4051
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3451
4052
  type: "button",
3452
4053
  className: "dab-btn dab-btn-ghost dab-btn-danger",
3453
4054
  onClick: () => setWp(null),
3454
4055
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("bgRemove")]
3455
4056
  })] }) : null]
3456
- })
4057
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4058
+ style: { marginTop: 16 },
4059
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4060
+ className: "dab-swatch-title",
4061
+ children: t("bgModeTitle")
4062
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4063
+ className: "dab-chip-row",
4064
+ children: BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4065
+ type: "button",
4066
+ className: `dab-chip${mode === m.mode ? " is-active" : ""}`,
4067
+ onClick: () => setMode(m.mode),
4068
+ children: t(m.labelKey)
4069
+ }, m.mode))
4070
+ })]
4071
+ })]
3457
4072
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3458
4073
  className: "dab-card dab-rise",
3459
4074
  style: { "--d": 3 },
@@ -3529,7 +4144,6 @@ window.__ModuleLoader__.load({
3529
4144
  step: 1,
3530
4145
  def: Math.round(generatedBg.scale * 100),
3531
4146
  fmt: (v) => `${v}%`,
3532
- onInput: () => {},
3533
4147
  onChange: (v) => updateGenerated({ scale: v / 100 })
3534
4148
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3535
4149
  label: t("bgMeshIntensity"),
@@ -3538,7 +4152,6 @@ window.__ModuleLoader__.load({
3538
4152
  step: 1,
3539
4153
  def: Math.round(generatedBg.intensity * 100),
3540
4154
  fmt: (v) => `${v}%`,
3541
- onInput: () => {},
3542
4155
  onChange: (v) => updateGenerated({ intensity: v / 100 })
3543
4156
  })] }) : null,
3544
4157
  activeGenType === "shader" && generatedBg?.type === "shader" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
@@ -3548,7 +4161,6 @@ window.__ModuleLoader__.load({
3548
4161
  step: 1,
3549
4162
  def: Math.round(generatedBg.speed * 100),
3550
4163
  fmt: (v) => `${v}%`,
3551
- onInput: () => {},
3552
4164
  onChange: (v) => updateGenerated({ speed: v / 100 })
3553
4165
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3554
4166
  label: t("bgShaderScale"),
@@ -3557,7 +4169,6 @@ window.__ModuleLoader__.load({
3557
4169
  step: 1,
3558
4170
  def: Math.round(generatedBg.scale * 100),
3559
4171
  fmt: (v) => `${v}%`,
3560
- onInput: () => {},
3561
4172
  onChange: (v) => updateGenerated({ scale: v / 100 })
3562
4173
  })] }) : null,
3563
4174
  activeGenType === "pattern" && generatedBg?.type === "pattern" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
@@ -3567,7 +4178,6 @@ window.__ModuleLoader__.load({
3567
4178
  step: 1,
3568
4179
  def: Math.round(generatedBg.density * 100),
3569
4180
  fmt: (v) => `${v}%`,
3570
- onInput: () => {},
3571
4181
  onChange: (v) => updateGenerated({ density: v / 100 })
3572
4182
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3573
4183
  label: t("bgPatternScale"),
@@ -3576,7 +4186,6 @@ window.__ModuleLoader__.load({
3576
4186
  step: 1,
3577
4187
  def: Math.round(generatedBg.scale * 100),
3578
4188
  fmt: (v) => `${v}%`,
3579
- onInput: () => {},
3580
4189
  onChange: (v) => updateGenerated({ scale: v / 100 })
3581
4190
  })] }) : null
3582
4191
  ]
@@ -3666,7 +4275,7 @@ window.__ModuleLoader__.load({
3666
4275
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3667
4276
  ref: fileRef,
3668
4277
  type: "file",
3669
- accept: "image/*",
4278
+ accept: "image/*,video/*",
3670
4279
  style: { display: "none" },
3671
4280
  onChange: (e) => {
3672
4281
  const f = e.target.files?.[0];
@@ -3675,18 +4284,20 @@ window.__ModuleLoader__.load({
3675
4284
  e.target.value = "";
3676
4285
  }
3677
4286
  }),
3678
- editorOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
4287
+ editorOpen && storeUrl && isStatic && mode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
3679
4288
  url: storeUrl,
3680
4289
  t,
3681
4290
  onClose: () => setEditorOpen(false),
3682
4291
  onCommit: (z, x, y, iw, ih) => {
3683
- cfg.bgState = {
4292
+ const st = {
3684
4293
  zoom: z,
3685
4294
  x,
3686
4295
  y,
3687
4296
  iw,
3688
4297
  ih
3689
4298
  };
4299
+ if (backgroundType === "video") cfg.videoBgState = st;
4300
+ else cfg.bgState = st;
3690
4301
  applyWp();
3691
4302
  saveConfig();
3692
4303
  setEditorOpen(false);
@@ -3908,25 +4519,9 @@ window.__ModuleLoader__.load({
3908
4519
  /**
3909
4520
  * dsh-any-background — browser half entry.
3910
4521
  *
3911
- * Appearance plugin with:
3912
- * 1. PS-style color wheel (hue ring + saturation/lightness square) for
3913
- * real-time theme color selection with dynamic token generation.
3914
- * 2. Background image editor modal with drag-to-pan and scroll-to-zoom
3915
- * inside a viewport-proportional preview rectangle.
3916
- * 3. Opacity / blur sliders with zero-lag direct DOM manipulation: the
3917
- * homepage background opacity (主界面) and the settings panel opacity
3918
- * (设置界面透明度) are separate sliders.
3919
- *
3920
- * This file wires the plugin lifecycle (theme registration, wallpaper layer,
3921
- * viewport watch, i18n, settings-section injection, boot restore, watchdog).
3922
- * The heavy lifting lives in the sibling modules:
3923
- * state.ts in-memory config mirror + getters
3924
- * rpc.ts file-backed persistence over the /dsh-any-background channel
3925
- * wallpaper.ts wallpaper DOM layer + inline token writes
3926
- * utils/ color math, token generation, image compression
3927
- * components/ ThemeSection shell + pages/, ColorWheel, BgEditor, LiveSlider
3928
- * i18n.ts zh/en dictionaries
3929
- * ui.css.ts scoped design-system stylesheet (classes + keyframes)
4522
+ * Wires the plugin lifecycle: theme registration, wallpaper layer, viewport
4523
+ * watch, i18n, settings-section injection, boot restore, watchdog. The heavy
4524
+ * lifting lives in the sibling modules (state/rpc/wallpaper/utils/components).
3930
4525
  */
3931
4526
  const name = "dsh-any-background";
3932
4527
  const inject = [
@@ -3959,13 +4554,10 @@ window.__ModuleLoader__.load({
3959
4554
  customDispose?.();
3960
4555
  if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
3961
4556
  }, "dsh-any-background: skin dispose");
3962
- let styleEl;
3963
- if (typeof document !== "undefined") {
3964
- styleEl = document.createElement("style");
3965
- styleEl.dataset.plugin = "dsh-any-background";
3966
- styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}`;
3967
- document.head.appendChild(styleEl);
3968
- }
4557
+ const styleEl = document.createElement("style");
4558
+ styleEl.dataset.plugin = "dsh-any-background";
4559
+ styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${TRAJECTORY_STYLE_RULE}`;
4560
+ document.head.appendChild(styleEl);
3969
4561
  ctx.effect(() => () => {
3970
4562
  styleEl?.parentNode?.removeChild(styleEl);
3971
4563
  }, "dsh-any-background: gradient");
@@ -4020,7 +4612,22 @@ window.__ModuleLoader__.load({
4020
4612
  const [h, s, l] = rColor();
4021
4613
  registerCustom(h, s, l);
4022
4614
  }
4023
- if (cfg.backgroundType !== "image") {
4615
+ if (cfg.backgroundType === "video") {
4616
+ const v = rWpVideo();
4617
+ if (v) {
4618
+ captureVideoSnapshot(v).then((snap) => {
4619
+ if (rWpVideo() !== v) return;
4620
+ setWpVideoSnapshot(snap);
4621
+ applyThemeColor();
4622
+ syncBg();
4623
+ });
4624
+ applyWp();
4625
+ } else {
4626
+ cfg.backgroundType = "image";
4627
+ setWpUrl(rWpImage());
4628
+ applyThemeColor();
4629
+ }
4630
+ } else if (cfg.backgroundType !== "image") {
4024
4631
  if (cfg.regenerateOnReload) regenerateGeneratedBg();
4025
4632
  else if (cfg.generatedBg) updateGeneratedBg(cfg.generatedBg);
4026
4633
  persistConfig();
@@ -4103,9 +4710,45 @@ window.__ModuleLoader__.load({
4103
4710
  setWpUrl(u);
4104
4711
  setBgState({ ...DEFAULT_CONFIG.bgState });
4105
4712
  persistWallpaper(u);
4713
+ if (u === null) {
4714
+ setWpVideoUrl(null, null);
4715
+ persistVideo(null);
4716
+ }
4106
4717
  applyThemeColor();
4107
4718
  syncBg();
4108
4719
  },
4720
+ setVideo: async (u, mime) => {
4721
+ setBgDark(null);
4722
+ if (u === null) {
4723
+ setWpVideoUrl(null, null);
4724
+ cfg.backgroundType = "image";
4725
+ setWpUrl(rWpImage());
4726
+ persistVideo(null);
4727
+ applyThemeColor();
4728
+ syncBg();
4729
+ saveConfig();
4730
+ return;
4731
+ }
4732
+ let ok;
4733
+ if (typeof u === "string") ok = await persistVideo(u);
4734
+ else ok = await uploadVideo(u, mime ?? u.type ?? "video/mp4");
4735
+ const live = ok ? VIDEO_SERVE_URL : typeof u === "string" ? u : null;
4736
+ if (live === null) return;
4737
+ cfg.backgroundType = "video";
4738
+ setWpUrl(null);
4739
+ cfg.videoBgState = { ...DEFAULT_CONFIG.bgState };
4740
+ setWpVideoUrl(live, mime ?? (typeof u === "string" ? null : u.type) ?? null);
4741
+ applyWp();
4742
+ saveConfig();
4743
+ syncBg();
4744
+ const applied = rWpVideo();
4745
+ captureVideoSnapshot(live).then((snap) => {
4746
+ if (rWpVideo() !== applied) return;
4747
+ setWpVideoSnapshot(snap);
4748
+ applyThemeColor();
4749
+ syncBg();
4750
+ });
4751
+ },
4109
4752
  setBgType: (type) => {
4110
4753
  setBackgroundType(type);
4111
4754
  saveConfig();
@@ -4158,7 +4801,7 @@ window.__ModuleLoader__.load({
4158
4801
  extractColor: async () => {
4159
4802
  const url = rWp();
4160
4803
  if (!url) return false;
4161
- const hsl = await extractWallpaperColor(url, rBgState());
4804
+ const hsl = await extractWallpaperColor(url, cfg.backgroundType === "video" ? rVideoBgState() : rBgState());
4162
4805
  if (!hsl) return false;
4163
4806
  cfg.color = hsl;
4164
4807
  registerCustom(hsl[0], hsl[1], hsl[2]);
@@ -4169,12 +4812,29 @@ window.__ModuleLoader__.load({
4169
4812
  bound?.syncColor(hsv, colorRev);
4170
4813
  return true;
4171
4814
  },
4172
- exportTheme: () => {
4815
+ exportTheme: async () => {
4816
+ let videoPayload = null;
4817
+ if (cfg.backgroundType === "video") {
4818
+ const vurl = rWpVideo();
4819
+ if (vurl) try {
4820
+ const blob = await fetch(vurl).then((r) => r.blob());
4821
+ videoPayload = await new Promise((resolve, reject) => {
4822
+ const fr = new FileReader();
4823
+ fr.onload = () => resolve(fr.result);
4824
+ fr.onerror = () => reject(fr.error);
4825
+ fr.readAsDataURL(blob);
4826
+ });
4827
+ if (videoPayload && !/^data:video\//.test(videoPayload)) videoPayload = videoPayload.replace(/^data:[^;,]*/, `data:${cfg.videoMime ?? "video/mp4"}`);
4828
+ } catch {
4829
+ videoPayload = null;
4830
+ }
4831
+ }
4173
4832
  const payload = {
4174
4833
  version: 2,
4175
4834
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4176
4835
  config: cfg,
4177
- wallpaper: cfg.backgroundType === "image" ? rWp() : null
4836
+ wallpaper: cfg.backgroundType === "image" ? rWp() : null,
4837
+ video: videoPayload
4178
4838
  };
4179
4839
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
4180
4840
  const url = URL.createObjectURL(blob);
@@ -4191,7 +4851,37 @@ window.__ModuleLoader__.load({
4191
4851
  const d = data;
4192
4852
  if (typeof d.config !== "object" || d.config === null) return false;
4193
4853
  adoptConfig(d.config);
4194
- if (cfg.backgroundType === "image") {
4854
+ if (cfg.backgroundType === "video") {
4855
+ const video = typeof d.video === "string" && /^data:video\//.test(d.video) ? d.video : null;
4856
+ if (video !== null) {
4857
+ let ok = false;
4858
+ try {
4859
+ const blob = await fetch(video).then((r) => r.blob());
4860
+ ok = await uploadVideo(blob, cfg.videoMime ?? blob.type ?? "video/mp4");
4861
+ } catch {
4862
+ ok = false;
4863
+ }
4864
+ if (!ok) ok = await persistVideo(video);
4865
+ const live = ok ? VIDEO_SERVE_URL : video;
4866
+ setWpVideoUrl(live, cfg.videoMime);
4867
+ applyWp();
4868
+ const applied = rWpVideo();
4869
+ captureVideoSnapshot(live).then((snap) => {
4870
+ if (rWpVideo() !== applied) return;
4871
+ setWpVideoSnapshot(snap);
4872
+ applyThemeColor();
4873
+ syncBg();
4874
+ });
4875
+ } else {
4876
+ setWpVideoUrl(null, null);
4877
+ persistVideo(null);
4878
+ cfg.backgroundType = "image";
4879
+ setWpImageUrl(null);
4880
+ setWpUrl(null);
4881
+ persistWallpaper(null);
4882
+ applyThemeColor();
4883
+ }
4884
+ } else if (cfg.backgroundType === "image") {
4195
4885
  const wallpaper = typeof d.wallpaper === "string" && /^data:image\//.test(d.wallpaper) ? d.wallpaper : null;
4196
4886
  setWpImageUrl(wallpaper);
4197
4887
  setWpUrl(wallpaper);