dsh-any-background 0.1.8 → 0.2.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
@@ -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: "在色轮外圈选择色相,在内部方形中调整饱和度和明度;双击滑块可恢复默认值",
@@ -32,20 +32,31 @@ window.__ModuleLoader__.load({
32
32
  uiOpacityBg: "主背景",
33
33
  uiOpacitySide: "侧边栏",
34
34
  uiOpacityCard: "对话框中选项面板",
35
+ uiOpacityInput: "输入框与控件",
35
36
  uiSop: "设置界面透明度",
37
+ uiChatRegion: "对话文本框",
38
+ uiTrajectory: "轨迹页",
36
39
  bgTitle: "背景",
37
- bgChoose: "选择图片",
40
+ bgChoose: "选择图片或视频",
38
41
  bgRemove: "移除背景",
39
42
  bgEdit: "编辑位置",
43
+ bgEditLocked: "仅「适应」模式可编辑位置",
40
44
  wpOpacity: "背景透明度",
41
45
  bgBlur: "背景模糊",
42
- bgSourceImage: "图片",
46
+ bgModeTitle: "布局模式",
47
+ bgModeFit: "适应",
48
+ bgModeFill: "填充",
49
+ bgModeStretch: "拉伸",
50
+ bgModeTile: "平铺",
51
+ bgModeCenter: "居中",
52
+ bgVideoBadge: "视频",
53
+ bgSourceImage: "图片 / 视频",
43
54
  bgSourceGenerated: "动态生成",
44
- dropHint: "点击选择图片,或将图片拖到这里",
55
+ dropHint: "点击选择图片或视频,或将文件拖到这里",
45
56
  liveBadge: "动态壁纸",
46
- bgHint: "图片模式下悬停预览可编辑位置;动态生成模式下可选择类型、预设与参数。绑定种子后刷新页面壁纸保持不变,取消绑定则每次刷新随机换新",
57
+ bgHint: "「适应」布局模式下悬停预览可编辑图片/视频的位置与缩放(其余布局模式按模式自动排布);动态生成模式下可选择类型、预设与参数。绑定种子后刷新页面壁纸保持不变,取消绑定则每次刷新随机换新",
47
58
  editorTitle: "背景编辑器",
48
- editorHint: "拖动移动图片,滚轮缩放大小",
59
+ editorHint: "拖动移动画面,滚轮缩放大小",
49
60
  editorCommit: "确认",
50
61
  editorCancel: "取消",
51
62
  editorReset: "重置",
@@ -107,7 +118,7 @@ window.__ModuleLoader__.load({
107
118
  pageProfile: "Profile",
108
119
  descColor: "Drag the wheel or type exact values — the whole UI follows live",
109
120
  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",
121
+ descBackground: "Upload an image or a video as wallpaper and place it freely, or let the algorithm paint a live background",
111
122
  descProfile: "Export the current theme as a file, or restore it from one",
112
123
  colorTitle: "Theme color",
113
124
  colorHint: "Pick hue on the outer ring, adjust saturation & lightness in the square; double-click a slider to reset",
@@ -119,18 +130,29 @@ window.__ModuleLoader__.load({
119
130
  uiOpacityBg: "Main background",
120
131
  uiOpacitySide: "Sidebar",
121
132
  uiOpacityCard: "Cards & panels",
133
+ uiOpacityInput: "Input & controls",
122
134
  uiSop: "Settings interface opacity",
135
+ uiChatRegion: "Conversation text frame",
136
+ uiTrajectory: "Trajectory view",
123
137
  bgTitle: "Background",
124
- bgChoose: "Choose image",
138
+ bgChoose: "Choose image or video",
125
139
  bgRemove: "Remove background",
126
140
  bgEdit: "Edit position",
141
+ bgEditLocked: "Position editing is only available in Fit mode",
127
142
  wpOpacity: "Background opacity",
128
143
  bgBlur: "Background blur",
129
- bgSourceImage: "Image",
144
+ bgModeTitle: "Layout mode",
145
+ bgModeFit: "Fit",
146
+ bgModeFill: "Fill",
147
+ bgModeStretch: "Stretch",
148
+ bgModeTile: "Tile",
149
+ bgModeCenter: "Center",
150
+ bgVideoBadge: "Video",
151
+ bgSourceImage: "Image / Video",
130
152
  bgSourceGenerated: "Generated",
131
- dropHint: "Click to choose an image, or drop one here",
153
+ dropHint: "Click to choose an image or video, or drop one here",
132
154
  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",
155
+ 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
156
  editorTitle: "Background editor",
135
157
  editorHint: "Drag to move, scroll to zoom",
136
158
  editorCommit: "Confirm",
@@ -191,13 +213,17 @@ window.__ModuleLoader__.load({
191
213
  opacities: {
192
214
  bg: .85,
193
215
  sidebar: .93,
194
- card: 1
216
+ card: 1,
217
+ input: 1
195
218
  },
196
219
  blurs: {
197
220
  bg: 0,
198
221
  sidebar: 0,
199
222
  card: 0,
200
- settings: 0
223
+ settings: 0,
224
+ chat: 0,
225
+ trajectory: 0,
226
+ input: 0
201
227
  },
202
228
  settingsOpacity: 1,
203
229
  wallpaperOpacity: 1,
@@ -209,24 +235,68 @@ window.__ModuleLoader__.load({
209
235
  iw: 0,
210
236
  ih: 0
211
237
  },
238
+ videoBgState: {
239
+ zoom: 1,
240
+ x: 0,
241
+ y: 0,
242
+ iw: 0,
243
+ ih: 0
244
+ },
212
245
  backgroundType: "image",
246
+ bgMode: "fit",
247
+ videoMime: null,
213
248
  generatedBg: null,
214
- regenerateOnReload: false
249
+ regenerateOnReload: false,
250
+ chatTextOpacity: 0,
251
+ trajectoryOpacity: 1
215
252
  };
253
+ const clamp01 = (n, def) => typeof n === "number" ? Math.min(1, Math.max(0, n)) : def;
216
254
  let cfg = {
217
255
  ...DEFAULT_CONFIG,
218
256
  opacities: { ...DEFAULT_CONFIG.opacities },
219
257
  blurs: { ...DEFAULT_CONFIG.blurs },
220
- bgState: { ...DEFAULT_CONFIG.bgState }
258
+ bgState: { ...DEFAULT_CONFIG.bgState },
259
+ videoBgState: { ...DEFAULT_CONFIG.videoBgState }
221
260
  };
222
261
  let wpImageUrl = null;
223
262
  let wpUrl = null;
263
+ let wpVideoUrl = null;
264
+ /** Captured video frame standing in for previews/color extraction (still-image APIs). */
265
+ let wpVideoSnapshot = null;
266
+ /** Local blob URL backing an in-session video; revoked when replaced or cleared. */
267
+ let wpVideoObjectUrl = null;
224
268
  function setWpImageUrl(url) {
225
269
  wpImageUrl = url;
226
270
  }
227
271
  function setWpUrl(url) {
228
272
  wpUrl = url;
229
273
  }
274
+ let videoRev = 0;
275
+ function setWpVideoUrl(url, mime) {
276
+ if (url === null) {
277
+ wpVideoUrl = null;
278
+ wpVideoSnapshot = null;
279
+ } else {
280
+ videoRev++;
281
+ wpVideoUrl = url.startsWith("blob:") ? url : `${url}${url.includes("?") ? "&" : "?"}r=${videoRev}`;
282
+ }
283
+ if (wpVideoObjectUrl !== null && wpVideoObjectUrl !== url) {
284
+ URL.revokeObjectURL(wpVideoObjectUrl);
285
+ wpVideoObjectUrl = null;
286
+ }
287
+ if (url !== null && url.startsWith("blob:")) wpVideoObjectUrl = url;
288
+ cfg.videoMime = url ? mime : null;
289
+ }
290
+ function setWpVideoSnapshot(url) {
291
+ wpVideoSnapshot = url;
292
+ }
293
+ /** Release the in-session video object URL (plugin teardown). */
294
+ function disposeVideoObjectUrl() {
295
+ if (wpVideoObjectUrl !== null) {
296
+ URL.revokeObjectURL(wpVideoObjectUrl);
297
+ wpVideoObjectUrl = null;
298
+ }
299
+ }
230
300
  function setBgState(s) {
231
301
  cfg.bgState = s;
232
302
  }
@@ -250,39 +320,78 @@ window.__ModuleLoader__.load({
250
320
  function rWpImage() {
251
321
  return wpImageUrl;
252
322
  }
253
- /** Display URL: the uploaded image when in image mode, else the generated snapshot. */
323
+ function rWpVideo() {
324
+ return wpVideoUrl;
325
+ }
326
+ function rBgMode() {
327
+ return cfg.bgMode ?? DEFAULT_CONFIG.bgMode;
328
+ }
329
+ function rChatTextOpacity() {
330
+ return clamp01(cfg.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity);
331
+ }
332
+ function rTrajectoryOpacity() {
333
+ return clamp01(cfg.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity);
334
+ }
335
+ /** Display URL: the uploaded image/video snapshot per active type, else the generated snapshot. */
254
336
  function rWp() {
255
- return cfg.backgroundType === "image" ? wpImageUrl : wpUrl;
337
+ if (cfg.backgroundType === "image") return wpImageUrl;
338
+ if (cfg.backgroundType === "video") return wpVideoSnapshot;
339
+ return wpUrl;
256
340
  }
257
341
  function rOps() {
258
342
  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
- };
343
+ const out = {};
344
+ for (const k of [
345
+ "bg",
346
+ "sidebar",
347
+ "card",
348
+ "input"
349
+ ]) out[k] = clamp01(o[k], DEFAULT_CONFIG.opacities[k]);
350
+ return out;
264
351
  }
265
352
  function rBlurs() {
266
353
  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
- };
354
+ const out = {};
355
+ for (const k of [
356
+ "bg",
357
+ "sidebar",
358
+ "card",
359
+ "settings",
360
+ "chat",
361
+ "trajectory",
362
+ "input"
363
+ ]) {
364
+ const v = b[k];
365
+ out[k] = typeof v === "number" ? Math.min(60, Math.max(0, v)) : DEFAULT_CONFIG.blurs[k];
366
+ }
367
+ return out;
273
368
  }
274
369
  function rWop() {
275
- return typeof cfg.wallpaperOpacity === "number" ? Math.min(1, Math.max(0, cfg.wallpaperOpacity)) : DEFAULT_CONFIG.wallpaperOpacity;
370
+ return clamp01(cfg.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity);
276
371
  }
277
372
  function rBl() {
278
373
  return typeof cfg.blur === "number" ? Math.min(60, Math.max(0, cfg.blur)) : DEFAULT_CONFIG.blur;
279
374
  }
280
375
  function rSop() {
281
- return typeof cfg.settingsOpacity === "number" ? Math.min(1, Math.max(0, cfg.settingsOpacity)) : DEFAULT_CONFIG.settingsOpacity;
376
+ return clamp01(cfg.settingsOpacity, DEFAULT_CONFIG.settingsOpacity);
282
377
  }
283
378
  function rBgState() {
284
379
  return cfg.bgState;
285
380
  }
381
+ function rVideoBgState() {
382
+ return cfg.videoBgState;
383
+ }
384
+ const num = (n, def) => typeof n === "number" ? n : def;
385
+ const cl = (n, lo, hi, def) => typeof n === "number" ? Math.min(hi, Math.max(lo, n)) : def;
386
+ function adoptBgState(s) {
387
+ return {
388
+ zoom: num(s.zoom, 1),
389
+ x: num(s.x, 0),
390
+ y: num(s.y, 0),
391
+ iw: typeof s.iw === "number" && s.iw > 0 ? s.iw : 0,
392
+ ih: typeof s.ih === "number" && s.ih > 0 ? s.ih : 0
393
+ };
394
+ }
286
395
  /** Move a possibly-absent partial config into the shape the UI reads. */
287
396
  function adoptConfig(raw) {
288
397
  const c = raw ?? {};
@@ -291,48 +400,64 @@ window.__ModuleLoader__.load({
291
400
  c.color[1],
292
401
  c.color[2]
293
402
  ] : null;
294
- const bg = c.bgState ?? {};
295
403
  const legacy = typeof c.opacity === "number" ? c.opacity : null;
296
404
  const ops = c.opacities ?? {};
297
405
  const bl = c.blurs ?? {};
298
- const bgType = c.backgroundType === "mesh" || c.backgroundType === "shader" || c.backgroundType === "pattern" ? c.backgroundType : DEFAULT_CONFIG.backgroundType;
406
+ const blurs = {};
407
+ for (const k of [
408
+ "bg",
409
+ "sidebar",
410
+ "card",
411
+ "settings",
412
+ "chat",
413
+ "trajectory",
414
+ "input"
415
+ ]) blurs[k] = num(bl[k], DEFAULT_CONFIG.blurs[k]);
416
+ const bgType = [
417
+ "video",
418
+ "mesh",
419
+ "shader",
420
+ "pattern"
421
+ ].includes(c.backgroundType) ? c.backgroundType : DEFAULT_CONFIG.backgroundType;
422
+ const bgMode = [
423
+ "fit",
424
+ "fill",
425
+ "stretch",
426
+ "tile",
427
+ "center"
428
+ ].includes(c.bgMode) ? c.bgMode : DEFAULT_CONFIG.bgMode;
299
429
  const gen = c.generatedBg && typeof c.generatedBg === "object" ? c.generatedBg : null;
300
430
  const generatedBg = gen && gen.type === bgType ? c.generatedBg : null;
301
431
  cfg = {
302
432
  color,
303
433
  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
434
+ bg: num(ops.bg, legacy ?? DEFAULT_CONFIG.opacities.bg),
435
+ sidebar: num(ops.sidebar, legacy !== null ? Math.min(1, legacy + .08) : DEFAULT_CONFIG.opacities.sidebar),
436
+ card: num(ops.card, DEFAULT_CONFIG.opacities.card),
437
+ input: num(ops.input, DEFAULT_CONFIG.opacities.input)
323
438
  },
439
+ blurs,
440
+ settingsOpacity: num(c.settingsOpacity, DEFAULT_CONFIG.settingsOpacity),
441
+ wallpaperOpacity: num(c.wallpaperOpacity, DEFAULT_CONFIG.wallpaperOpacity),
442
+ blur: num(c.blur, DEFAULT_CONFIG.blur),
443
+ bgState: adoptBgState(c.bgState ?? {}),
444
+ videoBgState: adoptBgState(c.videoBgState ?? {}),
324
445
  backgroundType: bgType,
446
+ bgMode,
447
+ videoMime: typeof c.videoMime === "string" ? c.videoMime : null,
325
448
  generatedBg: generatedBg ? normalizeGeneratedBg(generatedBg) : null,
326
- regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload
449
+ regenerateOnReload: typeof c.regenerateOnReload === "boolean" ? c.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
450
+ chatTextOpacity: clamp01(c.chatTextOpacity, DEFAULT_CONFIG.chatTextOpacity),
451
+ trajectoryOpacity: clamp01(c.trajectoryOpacity, DEFAULT_CONFIG.trajectoryOpacity)
327
452
  };
328
453
  }
329
454
  function normalizeGeneratedBg(p) {
330
455
  if (!p) return null;
331
456
  if (p.type === "mesh") return {
332
457
  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
458
+ seed: num(p.seed, 0),
459
+ scale: cl(p.scale, .3, 3, 1),
460
+ intensity: cl(p.intensity, 0, 1, .6)
336
461
  };
337
462
  if (p.type === "shader") return {
338
463
  type: "shader",
@@ -341,8 +466,8 @@ window.__ModuleLoader__.load({
341
466
  "nebula",
342
467
  "noise"
343
468
  ].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,
469
+ speed: cl(p.speed, 0, 2, .3),
470
+ scale: cl(p.scale, .3, 3, 1),
346
471
  seed: typeof p.seed === "number" ? Math.floor(p.seed) : 0
347
472
  };
348
473
  return {
@@ -352,14 +477,18 @@ window.__ModuleLoader__.load({
352
477
  "waves",
353
478
  "poly"
354
479
  ].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,
480
+ density: cl(p.density, 0, 1, .5),
481
+ scale: cl(p.scale, .3, 3, 1),
357
482
  seed: typeof p.seed === "number" ? Math.floor(p.seed) : 0
358
483
  };
359
484
  }
360
485
  //#endregion
361
486
  //#region src/client/rpc.ts
362
487
  const RPC_CHANNEL = "/dsh-any-background";
488
+ /** Same-origin serve URL of the persisted video (enough for <video src>/fetch). */
489
+ const VIDEO_SERVE_URL = "/dsh-any-background/video";
490
+ /** HTTP route new videos are POSTed to as raw bytes (see uploadVideo). */
491
+ const VIDEO_UPLOAD_URL = "/dsh-any-background/video/upload";
363
492
  const RPC_NS = "dshAnyBackground";
364
493
  const rpcEndpoint = (method) => `${RPC_NS}/${method}`;
365
494
  let rpcCallFn = null;
@@ -397,7 +526,7 @@ window.__ModuleLoader__.load({
397
526
  function persistConfig() {
398
527
  rpcCall("writeConfig", { config: cfg });
399
528
  }
400
- /** Load the persisted theme (config + wallpaper) from the node half. */
529
+ /** Load the persisted theme (config + wallpaper + video URL) from the node half. */
401
530
  async function loadPersisted() {
402
531
  const data = await rpcCall("read", {});
403
532
  if (data && typeof data === "object") {
@@ -405,13 +534,34 @@ window.__ModuleLoader__.load({
405
534
  if (d.config) adoptConfig(d.config);
406
535
  if (typeof d.wallpaper === "string") setWpImageUrl(d.wallpaper);
407
536
  else if (d.wallpaper === null) setWpImageUrl(null);
537
+ if (typeof d.videoUrl === "string") setWpVideoUrl(d.videoUrl, cfg.videoMime);
538
+ else if (d.videoUrl === null) setWpVideoUrl(null, null);
408
539
  if (cfg.backgroundType === "image") setWpUrl(d.wallpaper === null ? null : d.wallpaper);
409
540
  }
410
541
  }
411
- /** Persist a wallpaper (null removes it). One-shot large payload, no debounce. */
542
+ /** Persist a wallpaper (null removes it); one-shot, no debounce. */
412
543
  function persistWallpaper(dataUrl) {
413
544
  rpcCall("setWallpaper", { dataUrl });
414
545
  }
546
+ /** Persist a background video (null removes it); resolves true once on disk,
547
+ * so callers only switch playback to the serve URL after acceptance. */
548
+ async function persistVideo(dataUrl) {
549
+ return await rpcCall("setVideo", { dataUrl }) === true;
550
+ }
551
+ /** Upload a video's raw bytes over HTTP (MIME in Content-Type, body untouched
552
+ * — no base64 inflation that would blow the RPC body limit on large clips). */
553
+ async function uploadVideo(blob, mime) {
554
+ try {
555
+ return (await fetch(VIDEO_UPLOAD_URL, {
556
+ method: "POST",
557
+ headers: { "Content-Type": mime || "application/octet-stream" },
558
+ body: blob
559
+ })).ok;
560
+ } catch (e) {
561
+ console.warn("dsh-any-background: video upload failed", e);
562
+ return false;
563
+ }
564
+ }
415
565
  //#endregion
416
566
  //#region src/client/utils/color.ts
417
567
  function hsvToHsl(h, s, v) {
@@ -437,13 +587,8 @@ window.__ModuleLoader__.load({
437
587
  * same token set, and applyWp / applyCustomTokens / applySettingsOverrides call
438
588
  * this repeatedly (slider drags, viewport re-applies), so cache the last result
439
589
  * 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
590
  */
446
- function genTokens(hue, sat, lit, _palette) {
591
+ function genTokens(hue, sat, lit) {
447
592
  const key = `${hue}|${sat}|${lit}`;
448
593
  if (tokensCacheKey === key && tokensCache) return tokensCache;
449
594
  tokensCacheKey = key;
@@ -478,6 +623,7 @@ window.__ModuleLoader__.load({
478
623
  "--dsw-alias-button-primary-hover": hsl(h(0), s(.1), Math.max(l(.28), .58)),
479
624
  "--dsw-alias-button-primary-dimmed": hsl(h(0), s(0), l(.07)),
480
625
  "--dsw-alias-button-elevated-fill": hsl(h(0), s(0), l(.04)),
626
+ "--dsw-alias-button-floating-hover": hsl(h(0), s(0), l(.1)),
481
627
  "--dsw-alias-interactive-bg-hover": rgba(h(0), s(0), Math.max(l(.15), .4), .12),
482
628
  "--dsw-alias-interactive-bg-active": rgba(h(0), s(0), Math.max(l(.15), .4), .2),
483
629
  "--dsw-alias-markdown-code-block": hsl(h(0), s(0), l(-.06)),
@@ -489,6 +635,7 @@ window.__ModuleLoader__.load({
489
635
  "--dsw-specific-sidebar-nav-item-active": hsl(h(0), s(0), l(.04)),
490
636
  "--dsw-specific-sidebar-nav-item-hover": hsl(h(0), s(0), l(0)),
491
637
  "--dsw-specific-input-major": hsl(h(0), s(0), l(.02)),
638
+ "--dsw-specific-menu": hsl(h(0), s(0), l(.08)),
492
639
  "--dsw-alias-scrollbar-bg-l1": hsl(h(0), s(-.05), l(.12)),
493
640
  "--dsw-alias-scrollbar-bg-l2": hsl(h(0), s(-.05), l(.16)),
494
641
  "--dsw-alias-scrollbar-hover-l1": hsl(h(0), s(-.05), l(.22)),
@@ -516,6 +663,7 @@ window.__ModuleLoader__.load({
516
663
  "--dsw-alias-button-primary-hover": hsl(h(0), s(.05), Math.min(l(-.12), .5)),
517
664
  "--dsw-alias-button-primary-dimmed": hsl(h(0), s(-.1), l(-.03)),
518
665
  "--dsw-alias-button-elevated-fill": hsl(h(0), s(-.1), l(.1)),
666
+ "--dsw-alias-button-floating-hover": hsl(h(0), s(-.1), l(.16)),
519
667
  "--dsw-alias-interactive-bg-hover": rgba(h(0), s(0), l(-.3), .08),
520
668
  "--dsw-alias-interactive-bg-active": rgba(h(0), s(0), l(-.3), .14),
521
669
  "--dsw-alias-markdown-code-block": hsl(h(0), s(-.1), l(-.03)),
@@ -524,6 +672,7 @@ window.__ModuleLoader__.load({
524
672
  "--dsw-specific-sidebar-nav-item-active": hsl(h(0), s(-.08), l(.05)),
525
673
  "--dsw-specific-sidebar-nav-item-hover": hsl(h(0), s(-.12), l(0)),
526
674
  "--dsw-specific-input-major": hsl(h(0), s(-.12), l(.1)),
675
+ "--dsw-specific-menu": hsl(h(0), s(-.12), l(.15)),
527
676
  "--dsw-alias-scrollbar-bg-l1": hsl(h(0), s(-.1), l(-.08)),
528
677
  "--dsw-alias-scrollbar-bg-l2": hsl(h(0), s(-.08), l(-.12)),
529
678
  "--dsw-alias-scrollbar-hover-l1": hsl(h(0), s(-.08), l(-.16)),
@@ -1313,6 +1462,7 @@ window.__ModuleLoader__.load({
1313
1462
  //#endregion
1314
1463
  //#region src/client/wallpaper.ts
1315
1464
  let wpEl = null;
1465
+ let videoEl = null;
1316
1466
  let appliedTokenNames = [];
1317
1467
  let wpController = null;
1318
1468
  let snapshotListener = null;
@@ -1339,15 +1489,6 @@ window.__ModuleLoader__.load({
1339
1489
  for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1340
1490
  appliedTokenNames = [];
1341
1491
  }
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
1492
  /** Label tokens flipped by the generated-background brightness verdict. */
1352
1493
  const LABEL_TOKENS = [
1353
1494
  "--dsw-alias-label-primary",
@@ -1357,7 +1498,54 @@ window.__ModuleLoader__.load({
1357
1498
  "--dsw-alias-label-dimmed",
1358
1499
  "--dsw-alias-label-quaternary"
1359
1500
  ];
1501
+ const OPACITY_TOKEN_GROUPS = [
1502
+ {
1503
+ part: "bg",
1504
+ names: ["--dsw-alias-bg-base"]
1505
+ },
1506
+ {
1507
+ part: "sidebar",
1508
+ names: ["--dsw-specific-sidebar-fill"]
1509
+ },
1510
+ {
1511
+ part: "card",
1512
+ names: [
1513
+ "--dsw-alias-bg-layer-1",
1514
+ "--dsw-alias-bg-layer-2",
1515
+ "--dsw-alias-bg-layer-3",
1516
+ "--dsw-specific-menu"
1517
+ ]
1518
+ },
1519
+ {
1520
+ part: "input",
1521
+ names: ["--dsw-specific-input-major"]
1522
+ }
1523
+ ];
1524
+ const OPACITY_VARS = {
1525
+ "--dsw-alias-bg-base": "--dsh-any-op-bg",
1526
+ "--dsw-specific-sidebar-fill": "--dsh-any-op-sidebar",
1527
+ "--dsw-alias-bg-layer-1": "--dsh-any-op-card-1",
1528
+ "--dsw-alias-bg-layer-2": "--dsh-any-op-card-2",
1529
+ "--dsw-alias-bg-layer-3": "--dsh-any-op-card-3",
1530
+ "--dsw-specific-input-major": "--dsh-any-op-input",
1531
+ "--dsw-specific-menu": "--dsh-any-op-menu"
1532
+ };
1533
+ let baseTokenKey = "";
1534
+ let pendingOps = null;
1535
+ let tokensRaf = null;
1360
1536
  function applyCustomTokens(ops) {
1537
+ pendingOps = ops;
1538
+ if (tokensRaf !== null) return;
1539
+ tokensRaf = requestAnimationFrame(() => {
1540
+ tokensRaf = null;
1541
+ if (pendingOps === null) return;
1542
+ const o = pendingOps;
1543
+ pendingOps = null;
1544
+ applyCustomTokensNow(o);
1545
+ });
1546
+ }
1547
+ let lastBgKey = "";
1548
+ function applyCustomTokensNow(ops) {
1361
1549
  const [h, s, l] = rColor();
1362
1550
  let { tokens } = genTokens(h, s, l);
1363
1551
  try {
@@ -1367,49 +1555,68 @@ window.__ModuleLoader__.load({
1367
1555
  const font = dark ? "#fff" : "#000";
1368
1556
  for (const name of LABEL_TOKENS) tokens[name] = font;
1369
1557
  }
1370
- if (dark ?? l < .55) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
1371
- else document.body.removeAttribute("data-ds-dark-theme");
1372
- const decls = [];
1373
- for (const [name, value] of Object.entries(tokens)) {
1374
- let v = value;
1375
- if (name === "--dsw-alias-bg-base") v = toRgba(value, ops.bg);
1376
- else if (name === "--dsw-specific-sidebar-fill") v = toRgba(value, ops.sidebar);
1377
- else if (name === "--dsw-alias-bg-layer-1" || name === "--dsw-alias-bg-layer-2" || name === "--dsw-alias-bg-layer-3") v = toRgba(value, ops.card);
1378
- decls.push(`${name}:${v}!important`);
1558
+ const forceDark = dark ?? l < .55;
1559
+ if (`${h}|${s}|${l}|${dark}` !== baseTokenKey) {
1560
+ baseTokenKey = `${h}|${s}|${l}|${dark}`;
1561
+ if (forceDark) document.body.setAttribute("data-ds-dark-theme", "dsh-any-background");
1562
+ else document.body.removeAttribute("data-ds-dark-theme");
1563
+ const decls = [`color-scheme:${forceDark ? "dark" : "light"}`];
1564
+ for (const [name, value] of Object.entries(tokens)) {
1565
+ const opVar = OPACITY_VARS[name];
1566
+ decls.push(`${name}:${opVar !== void 0 ? `var(${opVar})` : value}!important`);
1567
+ }
1568
+ ensureTokenStyle().textContent = `body{${decls.join(";")}}`;
1569
+ for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1570
+ appliedTokenNames = Object.keys(tokens);
1379
1571
  }
1380
- ensureTokenStyle().textContent = `body{${decls.join(";")}}`;
1381
- for (const name of appliedTokenNames) document.body.style.removeProperty(name);
1382
- appliedTokenNames = Object.keys(tokens);
1383
- } catch (e) {}
1572
+ const root = document.documentElement;
1573
+ for (const g of OPACITY_TOKEN_GROUPS) for (const name of g.names) root.style.setProperty(OPACITY_VARS[name], toRgba(tokens[name] ?? "#000", ops[g.part]));
1574
+ root.style.setProperty("--dsh-any-op-menu-cordis", toRgba(tokens["--dsw-specific-menu"] ?? "#000", ops.input));
1575
+ const bgKey = `${baseTokenKey}|${ops.bg}`;
1576
+ if (bgKey !== lastBgKey) {
1577
+ lastBgKey = bgKey;
1578
+ applyPartOpacities(ops);
1579
+ }
1580
+ } catch {}
1581
+ }
1582
+ const SETTINGS_PANEL_SEL = "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]";
1583
+ 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)}`;
1584
+ const INPUT_BLUR_RULE = "[data-composer-card],[data-cordis-panel]{-webkit-backdrop-filter:var(--dsh-any-input-blur,none);backdrop-filter:var(--dsh-any-input-blur,none)}[data-cordis-panel]{--dsw-specific-menu:var(--dsh-any-op-menu-cordis)!important}";
1585
+ function applyInputBlur(px) {
1586
+ if (px > 0) document.documentElement.style.setProperty("--dsh-any-input-blur", `blur(${px}px)`);
1587
+ else document.documentElement.style.removeProperty("--dsh-any-input-blur");
1384
1588
  }
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)}`;
1386
1589
  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
1590
  const [h, s, l] = rColor();
1393
- const layer2 = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-2"];
1591
+ const tokens = genTokens(h, s, l).tokens;
1592
+ const layer1 = tokens["--dsw-alias-bg-layer-1"];
1593
+ const layer2 = tokens["--dsw-alias-bg-layer-2"];
1594
+ const layer3 = tokens["--dsw-alias-bg-layer-3"];
1394
1595
  if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-surface", toRgba(layer2, op));
1596
+ if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-1", toRgba(layer1, op));
1597
+ if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-2", toRgba(layer2, op));
1598
+ if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-bg-settings-layer-3", toRgba(layer3, op));
1395
1599
  }
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();
1600
+ 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)}";
1601
+ function applyTrajectoryOverrides(op) {
1602
+ const [h, s, l] = rColor();
1603
+ const tokens = genTokens(h, s, l).tokens;
1604
+ const layer1 = tokens["--dsw-alias-bg-layer-1"];
1605
+ const layer2 = tokens["--dsw-alias-bg-layer-2"];
1606
+ const layer3 = tokens["--dsw-alias-bg-layer-3"];
1607
+ if (layer1 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-1", toRgba(layer1, op));
1608
+ if (layer2 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-2", toRgba(layer2, op));
1609
+ if (layer3 !== void 0) document.documentElement.style.setProperty("--dsh-any-traj-layer-3", toRgba(layer3, op));
1401
1610
  }
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. */
1611
+ /** Apply the theme color: use the saved pick directly, or fall back to
1612
+ * extracting a dominant color from the current wallpaper. */
1406
1613
  function applyThemeColor() {
1407
1614
  if (rHasColor()) {
1408
1615
  applyWp();
1409
1616
  return;
1410
1617
  }
1411
1618
  const url = rWp();
1412
- if (url) extractWallpaperColor(url, rBgState()).then((hsl) => {
1619
+ if (url) extractWallpaperColor(url, cfg.backgroundType === "video" ? rVideoBgState() : rBgState()).then((hsl) => {
1413
1620
  if (hsl) cfg.color = hsl;
1414
1621
  applyWp();
1415
1622
  });
@@ -1426,6 +1633,13 @@ window.__ModuleLoader__.load({
1426
1633
  applyThemeColor();
1427
1634
  return;
1428
1635
  }
1636
+ if (type === "video") {
1637
+ clearDynamicBg();
1638
+ setBgDark(null);
1639
+ setWpUrl(null);
1640
+ applyThemeColor();
1641
+ return;
1642
+ }
1429
1643
  if (!cfg.generatedBg || cfg.generatedBg.type !== type) cfg.generatedBg = defaultParamsFor(type);
1430
1644
  applyGeneratedBg(cfg.generatedBg);
1431
1645
  }
@@ -1437,15 +1651,11 @@ window.__ModuleLoader__.load({
1437
1651
  function regenerateGeneratedBg() {
1438
1652
  const params = cfg.generatedBg;
1439
1653
  if (!params || cfg.backgroundType === "image") return;
1440
- const next = params.type === "mesh" ? {
1441
- ...params,
1442
- seed: randomSeed()
1443
- } : {
1654
+ cfg.generatedBg = {
1444
1655
  ...params,
1445
1656
  seed: randomSeed()
1446
1657
  };
1447
- cfg.generatedBg = next;
1448
- applyGeneratedBg(next);
1658
+ applyGeneratedBg(cfg.generatedBg);
1449
1659
  }
1450
1660
  /** Update a generated background's parameters and re-render. */
1451
1661
  function updateGeneratedBg(params) {
@@ -1455,6 +1665,7 @@ window.__ModuleLoader__.load({
1455
1665
  }
1456
1666
  function applyGeneratedBg(params) {
1457
1667
  clearDynamicBg();
1668
+ clearVideoEl();
1458
1669
  ensureWpContainer();
1459
1670
  wpController = createDynamicBackground(params);
1460
1671
  if (wpEl) {
@@ -1466,7 +1677,7 @@ window.__ModuleLoader__.load({
1466
1677
  const controller = wpController;
1467
1678
  const frame = controller.snapshot();
1468
1679
  setWpUrl(frame);
1469
- applyWallpaper();
1680
+ applyWp();
1470
1681
  snapshotListener?.();
1471
1682
  setBgDark(null);
1472
1683
  analyzeFrameDark(frame).then((dark) => {
@@ -1532,14 +1743,39 @@ window.__ModuleLoader__.load({
1532
1743
  if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-settings", `blur(${px}px)`);
1533
1744
  else document.documentElement.style.removeProperty("--dsh-any-blur-settings");
1534
1745
  }
1746
+ /** Apply the main-background opacity to the center/details columns instead of
1747
+ * the frame. The frame's translucent bg-base sits UNDER the sidebar, so
1748
+ * reducing the main-bg opacity stacked a second alpha onto the sidebar; moving
1749
+ * the alpha onto the columns keeps the sidebar owned by its own slider. */
1750
+ function applyPartOpacities(ops) {
1751
+ if (!(rHasColor() || rBgDark() !== null)) return;
1752
+ discoverParts();
1753
+ if (frameEl === null) return;
1754
+ const [h, s, l] = rColor();
1755
+ const base = genTokens(h, s, l).tokens["--dsw-alias-bg-base"];
1756
+ frameEl.style.background = "transparent";
1757
+ if (centerEl !== null) centerEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
1758
+ if (detailsEl !== null) detailsEl.style.background = base !== void 0 ? toRgba(base, ops.bg) : "transparent";
1759
+ }
1760
+ /** Blur of the option panels inside the settings dialog (.dab-card), owned by
1761
+ * the "dialog option panel" (card) blur slider. Written as a plugin-owned
1762
+ * variable consumed by SETTINGS_STYLE_RULE — deliberately NOT applied to the
1763
+ * homepage center/details columns, which this slider must never touch. */
1764
+ function applyCardPanelsBlur(px) {
1765
+ if (px > 0) document.documentElement.style.setProperty("--dsh-any-blur-card-panels", `blur(${px}px)`);
1766
+ else document.documentElement.style.removeProperty("--dsh-any-blur-card-panels");
1767
+ }
1535
1768
  /** Apply per-part interface blur to the AppFrame columns + settings panel. */
1536
1769
  function applyPartBlurs(blurs) {
1537
1770
  discoverParts();
1538
- setBlur(frameEl, blurs.bg);
1771
+ setBlur(frameEl, 0);
1539
1772
  setBlur(sidebarEl, blurs.sidebar);
1540
- setBlur(centerEl, blurs.card);
1541
- setBlur(detailsEl, blurs.card);
1773
+ setBlur(centerEl, blurs.bg);
1774
+ setBlur(detailsEl, blurs.bg);
1775
+ applyCardPanelsBlur(blurs.card);
1542
1776
  applySettingsBlur(blurs.settings);
1777
+ applyInputBlur(blurs.input);
1778
+ applyViewCards();
1543
1779
  }
1544
1780
  /** Live per-part blur update during slider drag (no full re-apply). */
1545
1781
  function setPartBlur(part, v) {
@@ -1547,13 +1783,182 @@ window.__ModuleLoader__.load({
1547
1783
  applySettingsBlur(v);
1548
1784
  return;
1549
1785
  }
1786
+ if (part === "card") {
1787
+ applyCardPanelsBlur(v);
1788
+ return;
1789
+ }
1790
+ if (part === "input") {
1791
+ applyInputBlur(v);
1792
+ return;
1793
+ }
1794
+ if (part === "chat" || part === "trajectory") {
1795
+ applyViewCards();
1796
+ return;
1797
+ }
1550
1798
  discoverParts();
1551
- if (part === "bg") setBlur(frameEl, v);
1552
- else if (part === "sidebar") setBlur(sidebarEl, v);
1553
- else {
1799
+ if (part === "bg") {
1554
1800
  setBlur(centerEl, v);
1555
1801
  setBlur(detailsEl, v);
1802
+ } else setBlur(sidebarEl, v);
1803
+ }
1804
+ const VIEW_CARDS = [{
1805
+ sel: "[data-chat-flow]",
1806
+ mark: "data-dab-chat-card",
1807
+ prev: "dabChatPrev",
1808
+ opacity: rChatTextOpacity,
1809
+ blur: () => rBlurs().chat,
1810
+ fallback: true
1811
+ }, {
1812
+ sel: "[data-conversation-composer-overlay]",
1813
+ mark: "data-dab-traj-card",
1814
+ prev: "dabTrajPrev",
1815
+ opacity: rTrajectoryOpacity,
1816
+ blur: () => rBlurs().trajectory,
1817
+ plain: true
1818
+ }];
1819
+ const viewTargets = VIEW_CARDS.map(() => null);
1820
+ function isScrollableY(el) {
1821
+ const oy = getComputedStyle(el).overflowY;
1822
+ return oy === "auto" || oy === "scroll" || oy === "overlay";
1823
+ }
1824
+ /** Whether the subtree hosts the chat input (textarea / contenteditable /
1825
+ * textbox role) — used to keep the card off the input row. */
1826
+ function containsChatEditor(el) {
1827
+ return el.querySelector("textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[contenteditable=\"plaintext-only\"],[role=\"textbox\"]") !== null;
1828
+ }
1829
+ /** Walk down from a coarse candidate toward the actual message column: stop
1830
+ * at a scroll container (the card surface must stay pinned to the scroll
1831
+ * port); while the chat input lives inside, descend into the tallest child that
1832
+ * does NOT contain it (the header row is short, the input row holds the
1833
+ * editor); otherwise peel wrappers dominated (>= 85%) by a single child so
1834
+ * tab bars / titles stay outside the card. */
1835
+ function refineMessageColumn(start) {
1836
+ let cur = start;
1837
+ for (let depth = 0; depth < 10; depth++) {
1838
+ if (isScrollableY(cur)) break;
1839
+ const kids = Array.from(cur.children).filter((k) => k instanceof HTMLElement);
1840
+ if (kids.length === 0) break;
1841
+ const tallest = kids.reduce((a, b) => b.clientHeight > a.clientHeight ? b : a);
1842
+ if (containsChatEditor(cur)) {
1843
+ const candidates = kids.filter((k) => !containsChatEditor(k) && k.clientHeight >= cur.clientHeight * .4);
1844
+ if (candidates.length === 0) break;
1845
+ cur = candidates.reduce((a, b) => b.clientHeight > a.clientHeight ? b : a);
1846
+ continue;
1847
+ }
1848
+ if (kids.length > 1 && tallest.clientHeight >= cur.clientHeight * .85) {
1849
+ cur = tallest;
1850
+ continue;
1851
+ }
1852
+ break;
1556
1853
  }
1854
+ return cur;
1855
+ }
1856
+ function discoverViewTarget(idx, spec) {
1857
+ if (centerEl === null || !document.body.contains(centerEl)) {
1858
+ viewTargets[idx] = null;
1859
+ return null;
1860
+ }
1861
+ const marked = centerEl.querySelector(spec.sel);
1862
+ const cached = viewTargets[idx];
1863
+ if (marked !== null) {
1864
+ if (cached !== null && cached !== marked) {
1865
+ setBlur(cached, 0);
1866
+ restoreCardHost(cached, spec.mark, spec.prev, spec.plain === true);
1867
+ }
1868
+ viewTargets[idx] = marked;
1869
+ return marked;
1870
+ }
1871
+ if (cached !== null && centerEl.contains(cached)) return cached;
1872
+ viewTargets[idx] = null;
1873
+ if (spec.fallback !== true) return null;
1874
+ if (centerEl.querySelector("[data-conversation-scroll]") !== null) return null;
1875
+ if (spec.opacity() <= 0 && spec.blur() <= 0) return null;
1876
+ let best = null;
1877
+ let bestArea = 0;
1878
+ for (const el of Array.from(centerEl.querySelectorAll("*"))) {
1879
+ if (!isScrollableY(el)) continue;
1880
+ if (el.clientHeight < centerEl.clientHeight * .35) continue;
1881
+ const area = el.clientWidth * el.clientHeight;
1882
+ if (area > bestArea) {
1883
+ bestArea = area;
1884
+ best = el;
1885
+ }
1886
+ }
1887
+ if (best === null) for (const el of Array.from(centerEl.children)) {
1888
+ if (!(el instanceof HTMLElement)) continue;
1889
+ if (el.clientHeight < centerEl.clientHeight * .5) continue;
1890
+ if (el.clientHeight > (best?.clientHeight ?? 0)) best = el;
1891
+ }
1892
+ const refined = best !== null ? refineMessageColumn(best) : null;
1893
+ viewTargets[idx] = refined;
1894
+ return refined;
1895
+ }
1896
+ /** Stash the host's own inline values so teardown restores them exactly.
1897
+ * Plain views only get a background override, so only that is stashed. */
1898
+ function stashCardPrev(el, prev, plain) {
1899
+ const ds = el.dataset;
1900
+ ds[prev + "Bg"] = el.style.getPropertyValue("background");
1901
+ if (plain) return;
1902
+ ds[prev + "Border"] = el.style.getPropertyValue("border");
1903
+ ds[prev + "Radius"] = el.style.getPropertyValue("border-radius");
1904
+ ds[prev + "Padding"] = el.style.getPropertyValue("padding");
1905
+ }
1906
+ /** Undo the inline styling, restoring the host's previous inline values. */
1907
+ function restoreCardHost(el, mark, prev, plain) {
1908
+ if (!el.hasAttribute(mark)) return;
1909
+ const ds = el.dataset;
1910
+ const restore = (prop, v) => {
1911
+ if (v !== void 0 && v !== "") el.style.setProperty(prop, v);
1912
+ else el.style.removeProperty(prop);
1913
+ };
1914
+ restore("background", ds[prev + "Bg"]);
1915
+ if (!plain) {
1916
+ restore("border", ds[prev + "Border"]);
1917
+ restore("border-radius", ds[prev + "Radius"]);
1918
+ restore("padding", ds[prev + "Padding"]);
1919
+ delete ds[prev + "Border"];
1920
+ delete ds[prev + "Radius"];
1921
+ delete ds[prev + "Padding"];
1922
+ }
1923
+ delete ds[prev + "Bg"];
1924
+ el.removeAttribute(mark);
1925
+ }
1926
+ /** Teardown only: strip every view treatment and hand the hosts back untouched. */
1927
+ function removeViewCards() {
1928
+ VIEW_CARDS.forEach((spec, i) => {
1929
+ const el = viewTargets[i];
1930
+ if (el !== null) {
1931
+ setBlur(el, 0);
1932
+ restoreCardHost(el, spec.mark, spec.prev, spec.plain === true);
1933
+ }
1934
+ viewTargets[i] = null;
1935
+ });
1936
+ }
1937
+ /** Re-derive the conversation view cards from the current config. Cheap
1938
+ * enough for live slider drags; the card structure is applied unconditionally
1939
+ * once the host exists so the layout never reflows when a slider leaves zero. */
1940
+ function applyViewCards() {
1941
+ discoverParts();
1942
+ if (centerEl === null) return;
1943
+ const [h, s, l] = rColor();
1944
+ const surface = genTokens(h, s, l).tokens["--dsw-alias-bg-layer-1"];
1945
+ VIEW_CARDS.forEach((spec, i) => {
1946
+ const target = discoverViewTarget(i, spec);
1947
+ if (target === null) return;
1948
+ const plain = spec.plain === true;
1949
+ const opacity = spec.opacity();
1950
+ const blurPx = spec.blur();
1951
+ if (!plain) {
1952
+ if (!target.hasAttribute(spec.mark)) stashCardPrev(target, spec.prev, false);
1953
+ const borderAlpha = opacity > 0 ? Math.min(1, opacity * 1.5) : blurPx > 0 ? .35 : 0;
1954
+ target.style.background = surface !== void 0 ? toRgba(surface, opacity) : "transparent";
1955
+ target.style.border = surface !== void 0 ? `1px solid ${toRgba(surface, borderAlpha)}` : "1px solid transparent";
1956
+ target.style.borderRadius = "16px";
1957
+ target.style.padding = "18px";
1958
+ }
1959
+ target.setAttribute(spec.mark, "1");
1960
+ setBlur(target, blurPx);
1961
+ });
1557
1962
  }
1558
1963
  let partsObserver = null;
1559
1964
  /** Watch for the AppFrame mounting so persisted blurs land even when the shell
@@ -1562,8 +1967,9 @@ window.__ModuleLoader__.load({
1562
1967
  function watchParts() {
1563
1968
  if (partsObserver !== null || typeof MutationObserver === "undefined") return;
1564
1969
  partsObserver = new MutationObserver(() => {
1565
- if (frameEl !== null && sidebarEl !== null && centerEl !== null && detailsEl !== null && document.body.contains(frameEl)) return;
1970
+ if (frameEl !== null && sidebarEl !== null && centerEl !== null && detailsEl !== null && document.body.contains(frameEl) && viewTargets.every((el) => el !== null && document.body.contains(el))) return;
1566
1971
  applyPartBlurs(rBlurs());
1972
+ applyPartOpacities(rOps());
1567
1973
  });
1568
1974
  partsObserver.observe(document.body, {
1569
1975
  childList: true,
@@ -1581,24 +1987,185 @@ window.__ModuleLoader__.load({
1581
1987
  document.body.prepend(wpEl);
1582
1988
  }
1583
1989
  }
1990
+ function clearVideoEl() {
1991
+ if (videoEl === null) return;
1992
+ videoEl.pause();
1993
+ videoEl.removeAttribute("src");
1994
+ videoEl.load();
1995
+ videoEl.remove();
1996
+ videoEl = null;
1997
+ }
1998
+ /** Intrinsic-size cache for the center mode (native pixels of the current image). */
1999
+ let imgNat = null;
2000
+ function imageNatSize(url, cb) {
2001
+ if (imgNat !== null && imgNat.url === url) {
2002
+ cb(imgNat.w, imgNat.h);
2003
+ return;
2004
+ }
2005
+ const img = new Image();
2006
+ img.onload = () => {
2007
+ imgNat = {
2008
+ url,
2009
+ w: img.naturalWidth,
2010
+ h: img.naturalHeight
2011
+ };
2012
+ cb(img.naturalWidth, img.naturalHeight);
2013
+ };
2014
+ img.onerror = () => cb(0, 0);
2015
+ img.src = url;
2016
+ }
2017
+ const DRAG_MAX_SIDE = 720;
2018
+ let lowResUrl = null;
2019
+ let lowResFor = "";
2020
+ let dragLow = false;
2021
+ function captureLowRes(url, cb) {
2022
+ if (lowResFor === url) {
2023
+ cb(lowResUrl);
2024
+ return;
2025
+ }
2026
+ const img = new Image();
2027
+ img.onload = () => {
2028
+ const k = Math.min(1, DRAG_MAX_SIDE / Math.max(img.naturalWidth, img.naturalHeight));
2029
+ if (k >= 1) {
2030
+ lowResFor = url;
2031
+ lowResUrl = null;
2032
+ cb(null);
2033
+ return;
2034
+ }
2035
+ const c = document.createElement("canvas");
2036
+ c.width = Math.max(1, Math.round(img.naturalWidth * k));
2037
+ c.height = Math.max(1, Math.round(img.naturalHeight * k));
2038
+ const g = c.getContext("2d");
2039
+ if (!g) {
2040
+ lowResFor = url;
2041
+ lowResUrl = null;
2042
+ cb(null);
2043
+ return;
2044
+ }
2045
+ g.drawImage(img, 0, 0, c.width, c.height);
2046
+ const low = c.toDataURL("image/jpeg", .85);
2047
+ lowResFor = url;
2048
+ lowResUrl = low;
2049
+ cb(low);
2050
+ };
2051
+ img.onerror = () => cb(null);
2052
+ img.src = url;
2053
+ }
2054
+ function setDragLow(on) {
2055
+ if (cfg.backgroundType !== "image" || on === dragLow || !wpEl) return;
2056
+ const full = rWpImage();
2057
+ if (!full) return;
2058
+ if (on) {
2059
+ dragLow = true;
2060
+ captureLowRes(full, (low) => {
2061
+ if (!dragLow || !wpEl || low === null) return;
2062
+ if (wpEl.style.backgroundImage !== `url("${low}")`) wpEl.style.backgroundImage = `url("${low}")`;
2063
+ });
2064
+ } else {
2065
+ dragLow = false;
2066
+ if (wpEl.style.backgroundImage !== `url("${full}")`) wpEl.style.backgroundImage = `url("${full}")`;
2067
+ }
2068
+ }
2069
+ /** While any range slider in the app is being dragged, run the wallpaper at
2070
+ * reduced resolution; restore on release. Returns a disposer for teardown. */
2071
+ function watchWallpaperDragQuality() {
2072
+ const isRange = (t) => t instanceof HTMLInputElement && t.type === "range";
2073
+ const down = (e) => {
2074
+ if (isRange(e.target)) setDragLow(true);
2075
+ };
2076
+ const up = () => {
2077
+ if (dragLow) setDragLow(false);
2078
+ };
2079
+ window.addEventListener("pointerdown", down, true);
2080
+ window.addEventListener("pointerup", up, true);
2081
+ window.addEventListener("pointercancel", up, true);
2082
+ return () => {
2083
+ window.removeEventListener("pointerdown", down, true);
2084
+ window.removeEventListener("pointerup", up, true);
2085
+ window.removeEventListener("pointercancel", up, true);
2086
+ if (dragLow) setDragLow(false);
2087
+ };
2088
+ }
1584
2089
  function applyImageWp(url) {
1585
2090
  clearDynamicBg();
2091
+ clearVideoEl();
1586
2092
  ensureWpContainer();
1587
2093
  const bg = rBgState();
2094
+ const mode = rBgMode();
1588
2095
  const next = `url("${url}")`;
1589
- if (wpEl.style.backgroundImage !== next) {
1590
- wpEl.style.backgroundImage = next;
2096
+ if (wpEl.style.backgroundImage !== next) wpEl.style.backgroundImage = next;
2097
+ if (mode === "fit") {
2098
+ wpEl.style.backgroundRepeat = "no-repeat";
2099
+ if (bg.iw > 0) {
2100
+ const fit = Math.min(window.innerWidth / bg.iw, window.innerHeight / bg.ih);
2101
+ const w = bg.iw * fit * bg.zoom;
2102
+ const h = bg.ih * fit * bg.zoom;
2103
+ wpEl.style.backgroundSize = `${w}px ${h}px`;
2104
+ wpEl.style.backgroundPosition = `${bg.x * window.innerWidth - w / 2}px ${bg.y * window.innerHeight - h / 2}px`;
2105
+ } else {
2106
+ wpEl.style.backgroundSize = "contain";
2107
+ wpEl.style.backgroundPosition = "center";
2108
+ }
2109
+ } else if (mode === "fill") {
2110
+ wpEl.style.backgroundRepeat = "no-repeat";
2111
+ wpEl.style.backgroundSize = "cover";
2112
+ wpEl.style.backgroundPosition = "center";
2113
+ } else if (mode === "stretch") {
1591
2114
  wpEl.style.backgroundRepeat = "no-repeat";
2115
+ wpEl.style.backgroundSize = "100% 100%";
2116
+ wpEl.style.backgroundPosition = "center";
2117
+ } else if (mode === "tile") {
2118
+ wpEl.style.backgroundRepeat = "repeat";
2119
+ wpEl.style.backgroundSize = "auto";
2120
+ wpEl.style.backgroundPosition = "0px 0px";
2121
+ } else {
2122
+ wpEl.style.backgroundRepeat = "no-repeat";
2123
+ wpEl.style.backgroundSize = "contain";
2124
+ wpEl.style.backgroundPosition = "center";
2125
+ imageNatSize(url, (w, h) => {
2126
+ if (!wpEl || wpEl.style.backgroundImage !== next || rBgMode() !== "center") return;
2127
+ if (w > 0 && h > 0) {
2128
+ wpEl.style.backgroundSize = `${w}px ${h}px`;
2129
+ wpEl.style.backgroundPosition = "center";
2130
+ }
2131
+ });
1592
2132
  }
1593
- if (bg.iw > 0) {
2133
+ captureLowRes(url, () => void 0);
2134
+ applyWpEffects();
2135
+ }
2136
+ /** Video wallpaper: a muted looping <video> inside the wallpaper layer.
2137
+ * Placement modes map onto object-fit (tile has no video equivalent and
2138
+ * falls back to cover). */
2139
+ function applyVideoWp(url) {
2140
+ clearDynamicBg();
2141
+ ensureWpContainer();
2142
+ if (wpEl.style.backgroundImage !== "none") wpEl.style.backgroundImage = "none";
2143
+ if (videoEl === null || !videoEl.isConnected) {
2144
+ videoEl = document.createElement("video");
2145
+ videoEl.muted = true;
2146
+ videoEl.loop = true;
2147
+ videoEl.autoplay = true;
2148
+ videoEl.playsInline = true;
2149
+ videoEl.preload = "auto";
2150
+ videoEl.setAttribute("playsinline", "");
2151
+ videoEl.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-position:center;";
2152
+ wpEl.appendChild(videoEl);
2153
+ videoEl.setAttribute("src", url);
2154
+ videoEl.play().catch(() => void 0);
2155
+ } else if (videoEl.getAttribute("src") !== url) {
2156
+ videoEl.setAttribute("src", url);
2157
+ videoEl.play().catch(() => void 0);
2158
+ }
2159
+ const mode = rBgMode();
2160
+ const bg = rVideoBgState();
2161
+ if (mode === "fit" && bg.iw > 0) {
1594
2162
  const fit = Math.min(window.innerWidth / bg.iw, window.innerHeight / bg.ih);
1595
2163
  const w = bg.iw * fit * bg.zoom;
1596
2164
  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`;
2165
+ 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
2166
  } else {
1600
- wpEl.style.backgroundSize = "contain";
1601
- wpEl.style.backgroundPosition = "center";
2167
+ videoEl.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-position:center;";
2168
+ videoEl.style.objectFit = mode === "stretch" ? "fill" : mode === "fill" || mode === "tile" ? "cover" : "contain";
1602
2169
  }
1603
2170
  applyWpEffects();
1604
2171
  }
@@ -1610,7 +2177,17 @@ window.__ModuleLoader__.load({
1610
2177
  }
1611
2178
  function applyWp() {
1612
2179
  const url = rWp();
1613
- if (cfg.backgroundType !== "image" && cfg.generatedBg) {
2180
+ if (cfg.backgroundType === "video") {
2181
+ const vurl = rWpVideo();
2182
+ if (vurl) applyVideoWp(vurl);
2183
+ else {
2184
+ clearDynamicBg();
2185
+ clearVideoEl();
2186
+ wpEl?.remove();
2187
+ wpEl = null;
2188
+ }
2189
+ } else if (cfg.backgroundType !== "image" && cfg.generatedBg) {
2190
+ clearVideoEl();
1614
2191
  if (!wpController) {
1615
2192
  applyGeneratedBg(cfg.generatedBg);
1616
2193
  return;
@@ -1621,27 +2198,56 @@ window.__ModuleLoader__.load({
1621
2198
  } else if (url) applyImageWp(url);
1622
2199
  else {
1623
2200
  clearDynamicBg();
2201
+ clearVideoEl();
1624
2202
  wpEl?.remove();
1625
2203
  wpEl = null;
1626
2204
  }
1627
2205
  if (rHasColor() || rBgDark() !== null) applyCustomTokens(rOps());
1628
- if (rHasColor()) applySettingsOverrides(rSop());
2206
+ if (rHasColor()) {
2207
+ applySettingsOverrides(rSop());
2208
+ applyTrajectoryOverrides(rTrajectoryOpacity());
2209
+ }
1629
2210
  applyPartBlurs(rBlurs());
1630
2211
  }
1631
2212
  function teardownWp() {
1632
2213
  clearDynamicBg();
2214
+ clearVideoEl();
2215
+ disposeVideoObjectUrl();
1633
2216
  setBgDark(null);
1634
2217
  wpEl?.remove();
1635
2218
  wpEl = null;
1636
2219
  clearCustomTokens();
1637
2220
  tokenStyleEl?.remove();
1638
2221
  tokenStyleEl = null;
2222
+ removeViewCards();
2223
+ document.body.removeAttribute("data-ds-dark-theme");
2224
+ document.body.style.removeProperty("color-scheme");
1639
2225
  document.documentElement.style.removeProperty("--dsh-any-bg-settings-surface");
2226
+ document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-1");
2227
+ document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-2");
2228
+ document.documentElement.style.removeProperty("--dsh-any-bg-settings-layer-3");
2229
+ document.documentElement.style.removeProperty("--dsh-any-traj-layer-1");
2230
+ document.documentElement.style.removeProperty("--dsh-any-traj-layer-2");
2231
+ document.documentElement.style.removeProperty("--dsh-any-traj-layer-3");
2232
+ document.documentElement.style.removeProperty("--dsh-any-bg-settings-card-surface");
1640
2233
  document.documentElement.style.removeProperty("--dsh-any-blur-settings");
2234
+ document.documentElement.style.removeProperty("--dsh-any-blur-card-panels");
2235
+ document.documentElement.style.removeProperty("--dsh-any-input-blur");
2236
+ for (const v of Object.values(OPACITY_VARS)) document.documentElement.style.removeProperty(v);
2237
+ baseTokenKey = "";
2238
+ lastBgKey = "";
2239
+ if (tokensRaf !== null) {
2240
+ cancelAnimationFrame(tokensRaf);
2241
+ tokensRaf = null;
2242
+ }
2243
+ pendingOps = null;
1641
2244
  setBlur(frameEl, 0);
1642
2245
  setBlur(sidebarEl, 0);
1643
2246
  setBlur(centerEl, 0);
1644
2247
  setBlur(detailsEl, 0);
2248
+ if (frameEl !== null) frameEl.style.removeProperty("background");
2249
+ if (centerEl !== null) centerEl.style.removeProperty("background");
2250
+ if (detailsEl !== null) detailsEl.style.removeProperty("background");
1645
2251
  stopWatchingParts();
1646
2252
  }
1647
2253
  /** Live wallpaper-opacity updates during slider drag (no full re-apply). */
@@ -1652,6 +2258,69 @@ window.__ModuleLoader__.load({
1652
2258
  function setWpBlur(v) {
1653
2259
  if (wpEl) wpEl.style.filter = v > 0 ? `blur(${v}px)` : "none";
1654
2260
  }
2261
+ //#endregion
2262
+ //#region src/client/utils/video.ts
2263
+ /**
2264
+ * Video-background helpers: capturing a single frame as a JPEG snapshot.
2265
+ * The snapshot stands in for everything that only understands still images:
2266
+ * the settings preview, theme-color extraction and the eyedropper. Uploads
2267
+ * stream the raw file over the binary upload route (see rpc.uploadVideo) —
2268
+ * a data-URL detour would inflate the bytes and trip the RPC body limit.
2269
+ */
2270
+ function grabFrame(v) {
2271
+ const w = v.videoWidth, h = v.videoHeight;
2272
+ if (w <= 0 || h <= 0) return null;
2273
+ const canvas = document.createElement("canvas");
2274
+ canvas.width = w;
2275
+ canvas.height = h;
2276
+ const ctx = canvas.getContext("2d");
2277
+ if (!ctx) return null;
2278
+ ctx.drawImage(v, 0, 0, w, h);
2279
+ try {
2280
+ return canvas.toDataURL("image/jpeg", .85);
2281
+ } catch {
2282
+ return null;
2283
+ }
2284
+ }
2285
+ /**
2286
+ * Capture one representative frame from a video data URL as a JPEG data URL.
2287
+ * Seeks to ~10% of the duration (first frame is often black) and waits for
2288
+ * the seek to land; resolves null when the video cannot be decoded. A timeout
2289
+ * keeps a broken file from hanging the upload flow.
2290
+ */
2291
+ function captureVideoSnapshot(url) {
2292
+ return new Promise((resolve) => {
2293
+ const v = document.createElement("video");
2294
+ let settled = false;
2295
+ const done = (r) => {
2296
+ if (settled) return;
2297
+ settled = true;
2298
+ window.clearTimeout(timer);
2299
+ v.removeAttribute("src");
2300
+ v.load();
2301
+ resolve(r);
2302
+ };
2303
+ const timer = window.setTimeout(() => done(grabFrame(v)), 6e3);
2304
+ v.muted = true;
2305
+ v.preload = "auto";
2306
+ v.playsInline = true;
2307
+ v.onerror = () => done(null);
2308
+ v.onloadeddata = () => {
2309
+ const target = isFinite(v.duration) && v.duration > .6 ? Math.min(v.duration * .1, 2) : 0;
2310
+ if (target <= 0) {
2311
+ done(grabFrame(v));
2312
+ return;
2313
+ }
2314
+ v.onseeked = () => done(grabFrame(v));
2315
+ try {
2316
+ v.currentTime = target;
2317
+ } catch {
2318
+ done(grabFrame(v));
2319
+ }
2320
+ };
2321
+ v.src = url;
2322
+ });
2323
+ }
1655
2324
  const UI_CSS = `
1656
2325
  /* The section is rendered INLINE inside the host settings dialog's content
1657
2326
  * column: the host provides the modal chrome (backdrop, centering, closing).
@@ -1934,6 +2603,44 @@ window.__ModuleLoader__.load({
1934
2603
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 10.4l-2.8-2.8-4.8 4.8" })
1935
2604
  ]
1936
2605
  });
2606
+ const VideoIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2607
+ size,
2608
+ className,
2609
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
2610
+ x: "1.5",
2611
+ y: "4.2",
2612
+ width: "8.8",
2613
+ height: "7.6",
2614
+ rx: "2"
2615
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10.3 6.9l4.2-2.4v7l-4.2-2.4" })]
2616
+ });
2617
+ const TextIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
2618
+ size,
2619
+ className,
2620
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 4.2h10M3 8h10M3 11.8h6.5" })
2621
+ });
2622
+ const TrajectoryIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2623
+ size,
2624
+ className,
2625
+ children: [
2626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 3.2h11M2.5 6.6h11M2.5 10h11" }),
2627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2628
+ cx: "4.4",
2629
+ cy: "3.2",
2630
+ r: "1.1"
2631
+ }),
2632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2633
+ cx: "8",
2634
+ cy: "6.6",
2635
+ r: "1.1"
2636
+ }),
2637
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2638
+ cx: "11.4",
2639
+ cy: "10",
2640
+ r: "1.1"
2641
+ })
2642
+ ]
2643
+ });
1937
2644
  const SlidersIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
1938
2645
  size,
1939
2646
  className,
@@ -2059,6 +2766,17 @@ window.__ModuleLoader__.load({
2059
2766
  r: "2.1"
2060
2767
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 1.7v1.8M8 12.5v1.8M1.7 8h1.8M12.5 8h1.8M3.6 3.6l1.3 1.3M11.1 11.1l1.3 1.3M12.4 3.6l-1.3 1.3M4.9 11.1l-1.3 1.3" })]
2061
2768
  });
2769
+ const InputIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2770
+ size,
2771
+ className,
2772
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
2773
+ x: "2.2",
2774
+ y: "4",
2775
+ width: "11.6",
2776
+ height: "8",
2777
+ rx: "2"
2778
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.6v2M7 8h2" })]
2779
+ });
2062
2780
  //#endregion
2063
2781
  //#region src/client/components/ErrorBoundary.tsx
2064
2782
  /**
@@ -2893,7 +3611,7 @@ window.__ModuleLoader__.load({
2893
3611
  paint(inputRef.current, v);
2894
3612
  }
2895
3613
  if (valRef.current) valRef.current.textContent = fmt(v);
2896
- onInput(v);
3614
+ onInput?.(v);
2897
3615
  onChange(v);
2898
3616
  };
2899
3617
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2922,7 +3640,7 @@ window.__ModuleLoader__.load({
2922
3640
  const el = e.target;
2923
3641
  const v = Number(el.value);
2924
3642
  paint(el, v);
2925
- onInput(v);
3643
+ onInput?.(v);
2926
3644
  if (valRef.current) valRef.current.textContent = fmt(v);
2927
3645
  },
2928
3646
  onChange: (e) => onChange(Number(e.target.value))
@@ -2952,10 +3670,25 @@ window.__ModuleLoader__.load({
2952
3670
  labelKey: "uiOpacityCard",
2953
3671
  Icon: ChatIcon
2954
3672
  },
3673
+ {
3674
+ opKey: "input",
3675
+ labelKey: "uiOpacityInput",
3676
+ Icon: InputIcon
3677
+ },
2955
3678
  {
2956
3679
  isSettings: true,
2957
3680
  labelKey: "uiSop",
2958
3681
  Icon: GearIcon
3682
+ },
3683
+ {
3684
+ isChat: true,
3685
+ labelKey: "uiChatRegion",
3686
+ Icon: TextIcon
3687
+ },
3688
+ {
3689
+ isTrajectory: true,
3690
+ labelKey: "uiTrajectory",
3691
+ Icon: TrajectoryIcon
2959
3692
  }
2960
3693
  ];
2961
3694
  function InterfacePage({ p }) {
@@ -2980,10 +3713,10 @@ window.__ModuleLoader__.load({
2980
3713
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2981
3714
  className: "dab-grid-parts",
2982
3715
  children: PARTS.map((part, i) => {
2983
- const { labelKey, Icon, isSettings } = part;
3716
+ const { labelKey, Icon, isSettings, isChat, isTrajectory } = part;
2984
3717
  const opKey = part.opKey;
2985
- const blurKey = opKey ?? "settings";
2986
- const opacity = isSettings ? rSop() : rOps()[opKey];
3718
+ const blurKey = isChat ? "chat" : isTrajectory ? "trajectory" : isSettings ? "settings" : opKey;
3719
+ const opacity = isChat ? rChatTextOpacity() : isTrajectory ? rTrajectoryOpacity() : isSettings ? rSop() : rOps()[opKey];
2987
3720
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2988
3721
  className: "dab-card dab-card-hover dab-rise",
2989
3722
  style: { "--d": i + 1 },
@@ -3014,7 +3747,13 @@ window.__ModuleLoader__.load({
3014
3747
  fmt: (v) => `${v}%`,
3015
3748
  onInput: (v) => {
3016
3749
  const op = v / 100;
3017
- if (isSettings) {
3750
+ if (isChat) {
3751
+ cfg.chatTextOpacity = op;
3752
+ applyViewCards();
3753
+ } else if (isTrajectory) {
3754
+ cfg.trajectoryOpacity = op;
3755
+ applyTrajectoryOverrides(op);
3756
+ } else if (isSettings) {
3018
3757
  cfg.settingsOpacity = op;
3019
3758
  applySettingsOverrides(op);
3020
3759
  } else {
@@ -3027,7 +3766,15 @@ window.__ModuleLoader__.load({
3027
3766
  },
3028
3767
  onChange: (v) => {
3029
3768
  const op = v / 100;
3030
- if (isSettings) setSop(op);
3769
+ if (isChat) {
3770
+ cfg.chatTextOpacity = op;
3771
+ applyViewCards();
3772
+ saveConfig();
3773
+ } else if (isTrajectory) {
3774
+ cfg.trajectoryOpacity = op;
3775
+ applyTrajectoryOverrides(op);
3776
+ saveConfig();
3777
+ } else if (isSettings) setSop(op);
3031
3778
  else {
3032
3779
  const ops = { ...rOps() };
3033
3780
  ops[opKey] = op;
@@ -3076,10 +3823,13 @@ window.__ModuleLoader__.load({
3076
3823
  }
3077
3824
  //#endregion
3078
3825
  //#region src/client/components/BgEditor.tsx
3826
+ /** Placement slot for the active background type: video framing lives in its
3827
+ * own state so image and video edits never overwrite each other. */
3828
+ const activeState = () => cfg.backgroundType === "video" ? cfg.videoBgState : cfg.bgState;
3079
3829
  function BgEditor({ url, t, onClose, onCommit }) {
3080
3830
  const pw = Math.min(window.innerWidth * .75, 860);
3081
3831
  const ph = Math.round(pw * window.innerHeight / window.innerWidth);
3082
- const saved = rBgState();
3832
+ const saved = activeState();
3083
3833
  const [zoom, setZoom] = (0, react.useState)(saved.iw > 0 ? saved.zoom : 1);
3084
3834
  const [pos, setPos] = (0, react.useState)(saved.iw > 0 ? {
3085
3835
  x: saved.x * pw,
@@ -3110,7 +3860,7 @@ window.__ModuleLoader__.load({
3110
3860
  w,
3111
3861
  h
3112
3862
  });
3113
- const s = rBgState();
3863
+ const s = activeState();
3114
3864
  if (s.iw > 0 && s.iw === img.width && s.ih === img.height) {
3115
3865
  setZoom(s.zoom);
3116
3866
  setPos({
@@ -3264,8 +4014,30 @@ window.__ModuleLoader__.load({
3264
4014
  //#endregion
3265
4015
  //#region src/client/components/pages/BackgroundPage.tsx
3266
4016
  const SEG_W = 108;
4017
+ const BG_MODES = [
4018
+ {
4019
+ mode: "fit",
4020
+ labelKey: "bgModeFit"
4021
+ },
4022
+ {
4023
+ mode: "fill",
4024
+ labelKey: "bgModeFill"
4025
+ },
4026
+ {
4027
+ mode: "stretch",
4028
+ labelKey: "bgModeStretch"
4029
+ },
4030
+ {
4031
+ mode: "tile",
4032
+ labelKey: "bgModeTile"
4033
+ },
4034
+ {
4035
+ mode: "center",
4036
+ labelKey: "bgModeCenter"
4037
+ }
4038
+ ];
3267
4039
  function BackgroundPage({ p }) {
3268
- const { t, setWp, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, useStore } = p;
4040
+ const { t, setWp, setVideo, setWop, setBl, setBgType, setGeneratedBg, regenerateBg, setRegenerateOnReload, useStore } = p;
3269
4041
  const store = useStore((s) => s);
3270
4042
  const storeUrl = store.url;
3271
4043
  const backgroundType = store.backgroundType;
@@ -3275,9 +4047,16 @@ window.__ModuleLoader__.load({
3275
4047
  const [editorOpen, setEditorOpen] = (0, react.useState)(false);
3276
4048
  const [dragOver, setDragOver] = (0, react.useState)(false);
3277
4049
  const [spinTick, setSpinTick] = (0, react.useState)(0);
3278
- const isGenerated = backgroundType !== "image";
4050
+ const [mode, setModeState] = (0, react.useState)(rBgMode());
4051
+ const isVideo = backgroundType === "video";
4052
+ const isStatic = backgroundType === "image" || isVideo;
4053
+ const isGenerated = !isStatic;
3279
4054
  const activeGenType = isGenerated && generatedBg ? generatedBg.type : "mesh";
3280
4055
  const onFileSelect = (f) => {
4056
+ if (f.type.startsWith("video/")) {
4057
+ setVideo(f, f.type);
4058
+ return;
4059
+ }
3281
4060
  readImg(f, (d) => {
3282
4061
  if (d) setWp(d);
3283
4062
  });
@@ -3286,10 +4065,18 @@ window.__ModuleLoader__.load({
3286
4065
  e.preventDefault();
3287
4066
  setDragOver(false);
3288
4067
  const f = e.dataTransfer.files?.[0];
3289
- if (f && f.type.startsWith("image/")) onFileSelect(f);
4068
+ if (f && (f.type.startsWith("image/") || f.type.startsWith("video/"))) onFileSelect(f);
4069
+ };
4070
+ const switchToStatic = () => {
4071
+ if (isStatic) return;
4072
+ setBgType(isVideo || cfg.videoMime !== null ? "video" : "image");
3290
4073
  };
3291
- const switchToImage = () => {
3292
- if (backgroundType !== "image") setBgType("image");
4074
+ const setMode = (m) => {
4075
+ if (m === mode) return;
4076
+ setModeState(m);
4077
+ cfg.bgMode = m;
4078
+ applyWp();
4079
+ saveConfig();
3293
4080
  };
3294
4081
  const setGenType = (type) => {
3295
4082
  if (type === activeGenType) return;
@@ -3367,15 +4154,20 @@ window.__ModuleLoader__.load({
3367
4154
  isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3368
4155
  className: "dab-hero-badge",
3369
4156
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, { size: 11 }), t("liveBadge")]
4157
+ }) : isVideo ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4158
+ className: "dab-hero-badge",
4159
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(VideoIcon, { size: 11 }), t("bgVideoBadge")]
3370
4160
  }) : null,
3371
4161
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3372
4162
  className: "dab-hero-veil",
3373
- children: [backgroundType === "image" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4163
+ children: [isStatic && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3374
4164
  type: "button",
3375
4165
  className: "dab-btn",
4166
+ disabled: mode !== "fit",
4167
+ title: mode !== "fit" ? t("bgEditLocked") : void 0,
3376
4168
  onClick: () => setEditorOpen(true),
3377
4169
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 13 }), t("bgEdit")]
3378
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4170
+ }) : isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3379
4171
  type: "button",
3380
4172
  className: "dab-btn",
3381
4173
  onClick: () => {
@@ -3383,7 +4175,7 @@ window.__ModuleLoader__.load({
3383
4175
  setSpinTick((x) => x + 1);
3384
4176
  },
3385
4177
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 13 }), t("bgRegenerate")]
3386
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4178
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3387
4179
  type: "button",
3388
4180
  className: "dab-btn dab-btn-danger",
3389
4181
  onClick: () => setWp(null),
@@ -3418,7 +4210,7 @@ window.__ModuleLoader__.load({
3418
4210
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3419
4211
  type: "button",
3420
4212
  className: `dab-seg-item${!isGenerated ? " is-active" : ""}`,
3421
- onClick: switchToImage,
4213
+ onClick: switchToStatic,
3422
4214
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhotoIcon, { size: 14 }), t("bgSourceImage")]
3423
4215
  }),
3424
4216
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -3432,28 +4224,44 @@ window.__ModuleLoader__.load({
3432
4224
  ]
3433
4225
  })
3434
4226
  }),
3435
- !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
4227
+ !isGenerated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3436
4228
  className: "dab-card dab-card-hover dab-rise",
3437
4229
  style: { "--d": 3 },
3438
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3439
4231
  className: "dab-chip-row",
3440
4232
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3441
4233
  type: "button",
3442
4234
  className: "dab-btn dab-btn-primary",
3443
4235
  onClick: () => fileRef.current?.click(),
3444
4236
  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", {
4237
+ }), storeUrl || isVideo ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [isStatic && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3446
4238
  type: "button",
3447
4239
  className: "dab-btn",
4240
+ disabled: mode !== "fit",
4241
+ title: mode !== "fit" ? t("bgEditLocked") : void 0,
3448
4242
  onClick: () => setEditorOpen(true),
3449
4243
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditIcon, { size: 14 }), t("bgEdit")]
3450
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4244
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3451
4245
  type: "button",
3452
4246
  className: "dab-btn dab-btn-ghost dab-btn-danger",
3453
4247
  onClick: () => setWp(null),
3454
4248
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("bgRemove")]
3455
4249
  })] }) : null]
3456
- })
4250
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4251
+ style: { marginTop: 16 },
4252
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4253
+ className: "dab-swatch-title",
4254
+ children: t("bgModeTitle")
4255
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4256
+ className: "dab-chip-row",
4257
+ children: BG_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4258
+ type: "button",
4259
+ className: `dab-chip${mode === m.mode ? " is-active" : ""}`,
4260
+ onClick: () => setMode(m.mode),
4261
+ children: t(m.labelKey)
4262
+ }, m.mode))
4263
+ })]
4264
+ })]
3457
4265
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3458
4266
  className: "dab-card dab-rise",
3459
4267
  style: { "--d": 3 },
@@ -3529,7 +4337,6 @@ window.__ModuleLoader__.load({
3529
4337
  step: 1,
3530
4338
  def: Math.round(generatedBg.scale * 100),
3531
4339
  fmt: (v) => `${v}%`,
3532
- onInput: () => {},
3533
4340
  onChange: (v) => updateGenerated({ scale: v / 100 })
3534
4341
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3535
4342
  label: t("bgMeshIntensity"),
@@ -3538,7 +4345,6 @@ window.__ModuleLoader__.load({
3538
4345
  step: 1,
3539
4346
  def: Math.round(generatedBg.intensity * 100),
3540
4347
  fmt: (v) => `${v}%`,
3541
- onInput: () => {},
3542
4348
  onChange: (v) => updateGenerated({ intensity: v / 100 })
3543
4349
  })] }) : null,
3544
4350
  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 +4354,6 @@ window.__ModuleLoader__.load({
3548
4354
  step: 1,
3549
4355
  def: Math.round(generatedBg.speed * 100),
3550
4356
  fmt: (v) => `${v}%`,
3551
- onInput: () => {},
3552
4357
  onChange: (v) => updateGenerated({ speed: v / 100 })
3553
4358
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3554
4359
  label: t("bgShaderScale"),
@@ -3557,7 +4362,6 @@ window.__ModuleLoader__.load({
3557
4362
  step: 1,
3558
4363
  def: Math.round(generatedBg.scale * 100),
3559
4364
  fmt: (v) => `${v}%`,
3560
- onInput: () => {},
3561
4365
  onChange: (v) => updateGenerated({ scale: v / 100 })
3562
4366
  })] }) : null,
3563
4367
  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 +4371,6 @@ window.__ModuleLoader__.load({
3567
4371
  step: 1,
3568
4372
  def: Math.round(generatedBg.density * 100),
3569
4373
  fmt: (v) => `${v}%`,
3570
- onInput: () => {},
3571
4374
  onChange: (v) => updateGenerated({ density: v / 100 })
3572
4375
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LiveSlider, {
3573
4376
  label: t("bgPatternScale"),
@@ -3576,7 +4379,6 @@ window.__ModuleLoader__.load({
3576
4379
  step: 1,
3577
4380
  def: Math.round(generatedBg.scale * 100),
3578
4381
  fmt: (v) => `${v}%`,
3579
- onInput: () => {},
3580
4382
  onChange: (v) => updateGenerated({ scale: v / 100 })
3581
4383
  })] }) : null
3582
4384
  ]
@@ -3666,7 +4468,7 @@ window.__ModuleLoader__.load({
3666
4468
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3667
4469
  ref: fileRef,
3668
4470
  type: "file",
3669
- accept: "image/*",
4471
+ accept: "image/*,video/*",
3670
4472
  style: { display: "none" },
3671
4473
  onChange: (e) => {
3672
4474
  const f = e.target.files?.[0];
@@ -3675,18 +4477,20 @@ window.__ModuleLoader__.load({
3675
4477
  e.target.value = "";
3676
4478
  }
3677
4479
  }),
3678
- editorOpen && storeUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
4480
+ editorOpen && storeUrl && isStatic && mode === "fit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BgEditor, {
3679
4481
  url: storeUrl,
3680
4482
  t,
3681
4483
  onClose: () => setEditorOpen(false),
3682
4484
  onCommit: (z, x, y, iw, ih) => {
3683
- cfg.bgState = {
4485
+ const st = {
3684
4486
  zoom: z,
3685
4487
  x,
3686
4488
  y,
3687
4489
  iw,
3688
4490
  ih
3689
4491
  };
4492
+ if (backgroundType === "video") cfg.videoBgState = st;
4493
+ else cfg.bgState = st;
3690
4494
  applyWp();
3691
4495
  saveConfig();
3692
4496
  setEditorOpen(false);
@@ -3908,25 +4712,9 @@ window.__ModuleLoader__.load({
3908
4712
  /**
3909
4713
  * dsh-any-background — browser half entry.
3910
4714
  *
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)
4715
+ * Wires the plugin lifecycle: theme registration, wallpaper layer, viewport
4716
+ * watch, i18n, settings-section injection, boot restore, watchdog. The heavy
4717
+ * lifting lives in the sibling modules (state/rpc/wallpaper/utils/components).
3930
4718
  */
3931
4719
  const name = "dsh-any-background";
3932
4720
  const inject = [
@@ -3959,16 +4747,15 @@ window.__ModuleLoader__.load({
3959
4747
  customDispose?.();
3960
4748
  if (colorTimerRef.current !== null) window.clearTimeout(colorTimerRef.current);
3961
4749
  }, "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
- }
4750
+ const styleEl = document.createElement("style");
4751
+ styleEl.dataset.plugin = "dsh-any-background";
4752
+ styleEl.textContent = `body[data-ds-dark-theme="dsh-any-background"]::before{content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(255,255,255,0.03) 0%,transparent 60%)}${SETTINGS_STYLE_RULE}${TRAJECTORY_STYLE_RULE}${INPUT_BLUR_RULE}`;
4753
+ document.head.appendChild(styleEl);
3969
4754
  ctx.effect(() => () => {
3970
4755
  styleEl?.parentNode?.removeChild(styleEl);
3971
4756
  }, "dsh-any-background: gradient");
4757
+ const disposeDragQuality = watchWallpaperDragQuality();
4758
+ ctx.effect(() => () => disposeDragQuality(), "dsh-any-background: drag quality");
3972
4759
  let rev = 0;
3973
4760
  let colorRev = 0;
3974
4761
  let bgRev = 0;
@@ -4020,7 +4807,22 @@ window.__ModuleLoader__.load({
4020
4807
  const [h, s, l] = rColor();
4021
4808
  registerCustom(h, s, l);
4022
4809
  }
4023
- if (cfg.backgroundType !== "image") {
4810
+ if (cfg.backgroundType === "video") {
4811
+ const v = rWpVideo();
4812
+ if (v) {
4813
+ captureVideoSnapshot(v).then((snap) => {
4814
+ if (rWpVideo() !== v) return;
4815
+ setWpVideoSnapshot(snap);
4816
+ applyThemeColor();
4817
+ syncBg();
4818
+ });
4819
+ applyWp();
4820
+ } else {
4821
+ cfg.backgroundType = "image";
4822
+ setWpUrl(rWpImage());
4823
+ applyThemeColor();
4824
+ }
4825
+ } else if (cfg.backgroundType !== "image") {
4024
4826
  if (cfg.regenerateOnReload) regenerateGeneratedBg();
4025
4827
  else if (cfg.generatedBg) updateGeneratedBg(cfg.generatedBg);
4026
4828
  persistConfig();
@@ -4068,6 +4870,25 @@ window.__ModuleLoader__.load({
4068
4870
  const sectionInject = (actions) => {
4069
4871
  bound = actions;
4070
4872
  syncBg();
4873
+ const playVideoFromBlob = (blob, mime) => {
4874
+ const localUrl = URL.createObjectURL(blob);
4875
+ cfg.backgroundType = "video";
4876
+ setWpUrl(null);
4877
+ cfg.videoBgState = { ...DEFAULT_CONFIG.bgState };
4878
+ setWpVideoUrl(localUrl, mime ?? blob.type ?? "video/mp4");
4879
+ applyWp();
4880
+ syncBg();
4881
+ const applied = rWpVideo();
4882
+ captureVideoSnapshot(localUrl).then((snap) => {
4883
+ if (rWpVideo() !== applied) return;
4884
+ setWpVideoSnapshot(snap);
4885
+ applyThemeColor();
4886
+ syncBg();
4887
+ });
4888
+ uploadVideo(blob, mime ?? blob.type ?? "video/mp4").then((ok) => {
4889
+ if (ok) persistConfig();
4890
+ });
4891
+ };
4071
4892
  const [wh, ws, wl] = rColor();
4072
4893
  const [dh, ds, dv] = hslToHsv(wh, ws, wl);
4073
4894
  return {
@@ -4103,9 +4924,45 @@ window.__ModuleLoader__.load({
4103
4924
  setWpUrl(u);
4104
4925
  setBgState({ ...DEFAULT_CONFIG.bgState });
4105
4926
  persistWallpaper(u);
4927
+ if (u === null) {
4928
+ setWpVideoUrl(null, null);
4929
+ persistVideo(null);
4930
+ }
4106
4931
  applyThemeColor();
4107
4932
  syncBg();
4108
4933
  },
4934
+ setVideo: async (u, mime) => {
4935
+ setBgDark(null);
4936
+ if (u === null) {
4937
+ setWpVideoUrl(null, null);
4938
+ cfg.backgroundType = "image";
4939
+ setWpUrl(rWpImage());
4940
+ persistVideo(null);
4941
+ applyThemeColor();
4942
+ syncBg();
4943
+ saveConfig();
4944
+ return;
4945
+ }
4946
+ if (typeof u !== "string") {
4947
+ playVideoFromBlob(u, mime);
4948
+ return;
4949
+ }
4950
+ const live = await persistVideo(u) ? VIDEO_SERVE_URL : u;
4951
+ cfg.backgroundType = "video";
4952
+ setWpUrl(null);
4953
+ cfg.videoBgState = { ...DEFAULT_CONFIG.bgState };
4954
+ setWpVideoUrl(live, mime ?? null);
4955
+ applyWp();
4956
+ saveConfig();
4957
+ syncBg();
4958
+ const applied = rWpVideo();
4959
+ captureVideoSnapshot(live).then((snap) => {
4960
+ if (rWpVideo() !== applied) return;
4961
+ setWpVideoSnapshot(snap);
4962
+ applyThemeColor();
4963
+ syncBg();
4964
+ });
4965
+ },
4109
4966
  setBgType: (type) => {
4110
4967
  setBackgroundType(type);
4111
4968
  saveConfig();
@@ -4158,7 +5015,7 @@ window.__ModuleLoader__.load({
4158
5015
  extractColor: async () => {
4159
5016
  const url = rWp();
4160
5017
  if (!url) return false;
4161
- const hsl = await extractWallpaperColor(url, rBgState());
5018
+ const hsl = await extractWallpaperColor(url, cfg.backgroundType === "video" ? rVideoBgState() : rBgState());
4162
5019
  if (!hsl) return false;
4163
5020
  cfg.color = hsl;
4164
5021
  registerCustom(hsl[0], hsl[1], hsl[2]);
@@ -4169,12 +5026,29 @@ window.__ModuleLoader__.load({
4169
5026
  bound?.syncColor(hsv, colorRev);
4170
5027
  return true;
4171
5028
  },
4172
- exportTheme: () => {
5029
+ exportTheme: async () => {
5030
+ let videoPayload = null;
5031
+ if (cfg.backgroundType === "video") {
5032
+ const vurl = rWpVideo();
5033
+ if (vurl) try {
5034
+ const blob = await fetch(vurl).then((r) => r.blob());
5035
+ videoPayload = await new Promise((resolve, reject) => {
5036
+ const fr = new FileReader();
5037
+ fr.onload = () => resolve(fr.result);
5038
+ fr.onerror = () => reject(fr.error);
5039
+ fr.readAsDataURL(blob);
5040
+ });
5041
+ if (videoPayload && !/^data:video\//.test(videoPayload)) videoPayload = videoPayload.replace(/^data:[^;,]*/, `data:${cfg.videoMime ?? "video/mp4"}`);
5042
+ } catch {
5043
+ videoPayload = null;
5044
+ }
5045
+ }
4173
5046
  const payload = {
4174
5047
  version: 2,
4175
5048
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4176
5049
  config: cfg,
4177
- wallpaper: cfg.backgroundType === "image" ? rWp() : null
5050
+ wallpaper: cfg.backgroundType === "image" ? rWp() : null,
5051
+ video: videoPayload
4178
5052
  };
4179
5053
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
4180
5054
  const url = URL.createObjectURL(blob);
@@ -4191,7 +5065,38 @@ window.__ModuleLoader__.load({
4191
5065
  const d = data;
4192
5066
  if (typeof d.config !== "object" || d.config === null) return false;
4193
5067
  adoptConfig(d.config);
4194
- if (cfg.backgroundType === "image") {
5068
+ if (cfg.backgroundType === "video") {
5069
+ const video = typeof d.video === "string" && /^data:video\//.test(d.video) ? d.video : null;
5070
+ if (video !== null) {
5071
+ let blob = null;
5072
+ try {
5073
+ blob = await fetch(video).then((r) => r.blob());
5074
+ } catch {
5075
+ blob = null;
5076
+ }
5077
+ if (blob !== null) playVideoFromBlob(blob, cfg.videoMime);
5078
+ else {
5079
+ const live = await persistVideo(video) ? VIDEO_SERVE_URL : video;
5080
+ setWpVideoUrl(live, cfg.videoMime);
5081
+ applyWp();
5082
+ const applied = rWpVideo();
5083
+ captureVideoSnapshot(live).then((snap) => {
5084
+ if (rWpVideo() !== applied) return;
5085
+ setWpVideoSnapshot(snap);
5086
+ applyThemeColor();
5087
+ syncBg();
5088
+ });
5089
+ }
5090
+ } else {
5091
+ setWpVideoUrl(null, null);
5092
+ persistVideo(null);
5093
+ cfg.backgroundType = "image";
5094
+ setWpImageUrl(null);
5095
+ setWpUrl(null);
5096
+ persistWallpaper(null);
5097
+ applyThemeColor();
5098
+ }
5099
+ } else if (cfg.backgroundType === "image") {
4195
5100
  const wallpaper = typeof d.wallpaper === "string" && /^data:image\//.test(d.wallpaper) ? d.wallpaper : null;
4196
5101
  setWpImageUrl(wallpaper);
4197
5102
  setWpUrl(wallpaper);