dsh-wallpaper_share 26.9.10 → 26.9.13

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
@@ -6,6 +6,121 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/DwpEditorCard.tsx
10
+ function DwpEditorCard() {
11
+ const [phase, setPhase] = (0, react.useState)("checking");
12
+ const [message, setMessage] = (0, react.useState)("");
13
+ const [opened, setOpened] = (0, react.useState)(false);
14
+ const alive = (0, react.useRef)(true);
15
+ const acquiring = (0, react.useRef)(false);
16
+ async function request(install = false) {
17
+ const r = await fetch("/we-sync/editor", install ? {
18
+ method: "POST",
19
+ headers: { "X-Wallpaper-Editor": "install" }
20
+ } : { cache: "no-store" });
21
+ const s = await r.json();
22
+ if (!r.ok) throw new Error(s.error || "无法连接编辑器服务,请刷新后重试。");
23
+ if (alive.current) setPhase(s.installed ? "installed" : s.installing ? "installing" : "available");
24
+ return s.installed;
25
+ }
26
+ (0, react.useEffect)(() => {
27
+ alive.current = true;
28
+ request().catch((e) => {
29
+ if (alive.current) {
30
+ setPhase("error");
31
+ setMessage(String(e.message));
32
+ }
33
+ });
34
+ return () => {
35
+ alive.current = false;
36
+ };
37
+ }, []);
38
+ (0, react.useEffect)(() => {
39
+ if (phase !== "installing" || acquiring.current) return;
40
+ const timer = setInterval(() => {
41
+ request().catch((e) => {
42
+ if (alive.current) {
43
+ setPhase("error");
44
+ setMessage(String(e.message));
45
+ }
46
+ });
47
+ }, 3e3);
48
+ return () => clearInterval(timer);
49
+ }, [phase]);
50
+ async function acquire() {
51
+ if (acquiring.current) return;
52
+ acquiring.current = true;
53
+ setMessage("");
54
+ setPhase("installing");
55
+ try {
56
+ if (await request(true) && alive.current) setOpened(true);
57
+ } catch (e) {
58
+ if (alive.current) {
59
+ setPhase("error");
60
+ setMessage(e instanceof Error ? e.message : "获取失败,请重试。");
61
+ }
62
+ } finally {
63
+ acquiring.current = false;
64
+ }
65
+ }
66
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
67
+ className: "wesync-card",
68
+ children: [
69
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
70
+ className: "wesync-sub",
71
+ children: "DWP 创作"
72
+ }),
73
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
74
+ style: {
75
+ display: "flex",
76
+ alignItems: "center",
77
+ gap: 12,
78
+ flexWrap: "wrap"
79
+ },
80
+ children: [
81
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
82
+ className: "wesync-btn",
83
+ disabled: phase === "checking" || phase === "installing",
84
+ onClick: () => phase === "installed" ? setOpened(true) : void acquire(),
85
+ children: phase === "checking" ? "检查编辑器…" : phase === "installing" ? "正在获取壁纸编辑器…" : phase === "installed" ? "打开壁纸编辑器" : "获取壁纸编辑器"
86
+ }),
87
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
88
+ style: {
89
+ opacity: .65,
90
+ fontSize: 12
91
+ },
92
+ children: phase === "installed" ? "已安装 · 可离线创作" : "24 帧创作 · 图像与视频 · 抠图 · 粒子 · 时钟"
93
+ }),
94
+ phase === "installed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
95
+ className: "wesync-btn",
96
+ href: "/we-sync/editor/app",
97
+ target: "_blank",
98
+ rel: "noreferrer",
99
+ children: "独立窗口"
100
+ })
101
+ ]
102
+ }),
103
+ message && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
104
+ role: "alert",
105
+ children: message
106
+ }),
107
+ opened && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
108
+ title: "壁纸编辑器",
109
+ src: "/we-sync/editor/app",
110
+ style: {
111
+ width: "100%",
112
+ height: "min(850px, 80vh)",
113
+ minHeight: 560,
114
+ border: 0,
115
+ marginTop: 12,
116
+ borderRadius: 8
117
+ },
118
+ allow: "fullscreen"
119
+ })
120
+ ]
121
+ });
122
+ }
123
+ //#endregion
9
124
  //#region src/client/GazeLens.ts
10
125
  /**
11
126
  * GazeLens —— 摄像头眼动追踪(透镜视线源)。
@@ -245,11 +360,48 @@ window.__ModuleLoader__.load({
245
360
  place();
246
361
  }
247
362
  //#endregion
363
+ //#region src/market/version.ts
364
+ /**
365
+ * 版本串比较(市场用):点分数字段逐段比大小,后缀(如 `-rc` / `-T` / `+build`)忽略。
366
+ *
367
+ * 为什么需要它:市场目录是**远端数据**(`raw.githubusercontent` 有 CDN 缓存、Actions 也要跑一会儿),
368
+ * 所以"目录里的版本比本机已装的旧"是真会出现的状态。拿它当"更新"用,就会把新包静默装回旧版本
369
+ * (2026-09-10 实际发生过一次:刚发布的 1.1.0 被目录里的 1.0.0 覆盖)。比较在这里统一实现,
370
+ * node 半(安装护栏)与 client 半(卡片按钮状态)共用同一套语义。
371
+ */
372
+ function compareVersion(a, b) {
373
+ const parse = (v) => v.trim().replace(/^v/i, "").split(".").map((seg) => {
374
+ const m = /^\d+/.exec(seg);
375
+ return m === null ? 0 : Number(m[0]);
376
+ });
377
+ const xs = parse(a);
378
+ const ys = parse(b);
379
+ const n = Math.max(xs.length, ys.length);
380
+ for (let i = 0; i < n; i++) {
381
+ const x = xs[i] ?? 0;
382
+ const y = ys[i] ?? 0;
383
+ if (x !== y) return x < y ? -1 : 1;
384
+ }
385
+ return 0;
386
+ }
387
+ //#endregion
248
388
  //#region src/client/market-api.ts
389
+ /**
390
+ * 市场前端的纯逻辑层(client 半,浏览器安全:只用 fetch,零 node 依赖)。
391
+ * 与 node 半 src/market/* 解耦——这里只重声明 UI 需要的类型子集,
392
+ * 避免把 node:crypto / node:fs 拖进 lib/client.js 浏览器包。
393
+ * 全部函数注入 fetch,Node 测试喂假响应即可跑通(无需浏览器)。
394
+ *
395
+ * 范围(本轮锁定):只做免费。付费条目在 buildCards 里直接过滤掉,
396
+ * 不渲染、不提供购买/授权流程(付费系统暂缓实装)。
397
+ */
249
398
  const defaultFetch$1 = (url, init) => fetch(url, init);
250
- /** 拉 catalog(node 半已缓存),返回条目数组。 */
251
- async function fetchCatalog(fetchFn = defaultFetch$1, url = "/we-sync/dwp/market/catalog") {
252
- const res = await fetchFn(url, { cache: "no-store" });
399
+ /**
400
+ * 拉 catalog(node 半缓存 5 分钟)。`refresh: true` = 强制重拉:node 半据此绕开自身缓存,
401
+ * 并给上游 URL 加 cache-buster(catalog 挂在 raw.githubusercontent 上,不加就还是旧目录)。
402
+ */
403
+ async function fetchCatalog(fetchFn = defaultFetch$1, url = "/we-sync/dwp/market/catalog", opts) {
404
+ const res = await fetchFn(opts?.refresh === true ? url + (url.includes("?") ? "&" : "?") + "refresh=1" : url, { cache: "no-store" });
253
405
  if (!res.ok) throw new Error(`catalog ${res.status}`);
254
406
  const body = await res.json();
255
407
  return Array.isArray(body.entries) ? body.entries : [];
@@ -261,14 +413,19 @@ window.__ModuleLoader__.load({
261
413
  const body = await res.json();
262
414
  return Array.isArray(body.installed) ? body.installed : [];
263
415
  }
264
- /** 合并 catalog + installed → 卡片视图模型。免费 only(commercial 过滤掉)。 */
416
+ /**
417
+ * 合并 catalog + installed → 卡片视图模型。免费 only(commercial 过滤掉)。
418
+ * 安装状态按版本比较得出:目录比本地新 = update;**目录比本地旧 = local-ahead**
419
+ * (远端目录可能因为 CDN 缓存还没刷新 —— 这种卡片不给"更新"按钮,避免把新包装回旧版本)。
420
+ */
265
421
  function buildCards(catalog, installed) {
266
422
  const byId = new Map(installed.map((i) => [i.id, i]));
267
423
  const cards = [];
268
424
  for (const entry of catalog) {
269
425
  if (entry.license.commercial !== false) continue;
270
426
  const rec = byId.get(entry.id);
271
- const state = rec === void 0 ? "absent" : rec.version !== entry.dwp.package.version ? "update" : "installed";
427
+ const next = entry.dwp.package.version;
428
+ const state = rec === void 0 ? "absent" : rec.version === next ? "installed" : compareVersion(next, rec.version) < 0 ? "local-ahead" : "update";
272
429
  cards.push({
273
430
  entry,
274
431
  state,
@@ -484,6 +641,63 @@ window.__ModuleLoader__.load({
484
641
  return (i === 0 ? String(v) : v.toFixed(1)) + " " + units[i];
485
642
  }
486
643
  //#endregion
644
+ //#region src/client/library-model.ts
645
+ const isApplication = (a) => a.type === "application";
646
+ const isLauncherApp = (a) => (a.source ?? "") === "launcher";
647
+ /** 应用大类切分:all 为全部应用;we 排除启动器装的;launcher 只留启动器装的。 */
648
+ function partitionApps(apps) {
649
+ const only = apps.filter(isApplication);
650
+ return {
651
+ all: only,
652
+ we: only.filter((a) => !isLauncherApp(a)),
653
+ launcher: only.filter(isLauncherApp)
654
+ };
655
+ }
656
+ /** 按二级分类取子集(all 返回全部应用)。 */
657
+ function appsForSub(apps, sub) {
658
+ return partitionApps(apps)[sub];
659
+ }
660
+ /**
661
+ * 目录路径取末段。scanApps 的 id 就是壁纸目录全路径(服务端 normalize 成 '/' 分隔),
662
+ * 启动器安装的应用目录恒为 `<安装根>/<slug>`,因此末段即 slug。
663
+ * 末尾有分隔符时也要正确取到(split + 过滤空段)。
664
+ */
665
+ function slugOfDir(dir) {
666
+ const parts = dir.replace(/\\/g, "/").split("/").filter((s) => s !== "");
667
+ return parts.length === 0 ? "" : parts[parts.length - 1];
668
+ }
669
+ /**
670
+ * 扫描到的「应用」瓷砖 → 启动器安装记录(用于给出「卸载」)。
671
+ * 优先按目录末段匹配 slug(唯一可信的对应关系);匹配不到时**只在服务端已标记
672
+ * source === 'launcher' 的前提下**按标题兜底(目录被重命名过的情况)。
673
+ *
674
+ * 标题兜底必须卡在 source 上:工坊 / 自定义目录里的应用和启动器应用完全可能同名,
675
+ * 若放开兜底,点工坊瓷砖的「卸载」会删掉另一条启动器记录——那是真实的数据损失。
676
+ * 返回 null 表示"不是启动器装的":这种瓷砖只给「打开源文件」。
677
+ */
678
+ function matchLauncherRecord(app, records) {
679
+ const slug = slugOfDir(app.id).toLowerCase();
680
+ if (slug !== "") {
681
+ const bySlug = records.find((r) => r.slug.toLowerCase() === slug);
682
+ if (bySlug !== void 0) return bySlug;
683
+ }
684
+ if (!isLauncherApp(app)) return null;
685
+ return records.find((r) => r.title === app.title) ?? null;
686
+ }
687
+ /** ISO 安装时间 → 本地 `YYYY-MM-DD HH:mm`(解析失败时原样回显,不吞信息)。 */
688
+ function formatInstalledAt(iso) {
689
+ const d = new Date(iso);
690
+ if (Number.isNaN(d.getTime())) return iso;
691
+ const p = (n) => String(n).padStart(2, "0");
692
+ return String(d.getFullYear()) + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) + " " + p(d.getHours()) + ":" + p(d.getMinutes());
693
+ }
694
+ /** 启动器应用目录(详情里的「地址」):安装根 + slug,统一 Windows 反斜杠。 */
695
+ function launcherAppDir(root, slug) {
696
+ const r = root.trim().replace(/[\\/]+$/, "");
697
+ if (r === "") return slug;
698
+ return r.replace(/\//g, "\\") + "\\" + slug;
699
+ }
700
+ //#endregion
487
701
  //#region src/client/WallpaperSharePanel.tsx
488
702
  /**
489
703
  * wallpaper_share 会话视图标签页:当前壁纸信息、同步开关、显示器选择、
@@ -515,6 +729,10 @@ window.__ModuleLoader__.load({
515
729
  focusIntro: "随任务自适应调节背景效果",
516
730
  flashFocusOn: "专注模式已开启:注视点透镜跟随鼠标(圆心清晰);可再开「眼动追踪」改为跟随视线",
517
731
  flashFocusOff: "专注模式已关闭,恢复手动滑块",
732
+ floaterMode: "桌面悬浮球",
733
+ flashFloaterOn: "桌面悬浮球已开启:本页切到后台 / 浏览器最小化时,桌面出现环形悬浮球,单击切回本页",
734
+ flashFloaterOff: "桌面悬浮球已关闭",
735
+ floaterUnsupported: "桌面悬浮球需要 Windows 宿主且随包携带 we-floater.exe(当前不可用)",
518
736
  renderModeTitle: "渲染模式",
519
737
  modeEco: "预览",
520
738
  modePerf: "捕获",
@@ -558,17 +776,38 @@ window.__ModuleLoader__.load({
558
776
  openFolderFailed: "打开文件夹失败",
559
777
  mountFailed: "挂载失败",
560
778
  typeDwp: "dwp壁纸",
561
- typeWeApp: "we 应用",
779
+ typeApps: "应用",
780
+ typeWeApp: "we应用",
562
781
  typeLauncherApp: "应用",
782
+ appSubAll: "全部",
783
+ manage: "管理",
784
+ manageDone: "完成",
785
+ openSource: "打开源文件",
786
+ detail: "详细",
787
+ detailHide: "收起",
788
+ detailInstalledAt: "安装时间",
789
+ detailAddr: "地址",
790
+ detailExe: "exe 文件",
791
+ launcherManageHint: "启动与卸载到「本地 → 应用」里操作。",
792
+ confirmUninstallTitle: "确认卸载?",
793
+ confirmUninstallBody: "将删除以下内容的本地文件(dwp 壁纸可在市场重装):",
794
+ manageSelected: "已选",
795
+ bulkUninstall: "卸载选中",
796
+ clearSel: "清空选择",
797
+ manageNotDeletable: "WE 工坊内容不在此卸载(可用「打开源文件」定位)",
798
+ confirmBulkTitle: (n) => "确认卸载选中的 " + String(n) + " 项?",
799
+ flashBulkUninstalled: (n) => "已卸载 " + String(n) + " 项",
800
+ flashBulkPartial: (n, fail) => "已卸载 " + String(n) + " 项,失败:" + fail,
563
801
  pageSettings: "设置",
564
802
  pageLibrary: "壁纸库",
803
+ pageDwp: "dwp创作",
565
804
  pageHint: "滚动切页 · 用力滚才翻页",
566
805
  pageGapHint: "继续滚动翻页 · 轻滑弹回",
567
806
  mounted: "已挂载",
568
807
  searchPlaceholder: "搜索标题…",
569
808
  showMore: "显示更多",
570
809
  dwpEmpty: "还没有已安装的 DWP 壁纸,去壁纸库「市场」一栏拉取。",
571
- weAppEmpty: "没有 WE 应用类壁纸。",
810
+ weAppEmpty: "没有应用类内容。",
572
811
  appsCount: (total, matched) => total === matched ? `共 ${String(total)} 个` : `共 ${String(total)} 个 · 匹配 ${String(matched)} 个`,
573
812
  marketRefresh: "刷新",
574
813
  marketSearch: "搜索名称 / 作者…",
@@ -578,6 +817,11 @@ window.__ModuleLoader__.load({
578
817
  marketUpdate: "更新",
579
818
  marketUninstall: "卸载",
580
819
  marketInstalled: "已安装",
820
+ marketLocalAhead: "本地更新",
821
+ marketLocalAheadHint: "本机版本比目录里的新(远端目录可能还没刷新);不提供回退,避免误装回旧版本",
822
+ dwpTierLead: "DWP 壁纸 · 纹理档:",
823
+ dwpTierHd: "高清(增强 / 完整)",
824
+ dwpTierSd: "标准(预览 / 捕获)",
581
825
  marketEmpty: "目录为空",
582
826
  marketLoading: "加载中…",
583
827
  marketNoMatch: "无匹配结果",
@@ -602,12 +846,12 @@ window.__ModuleLoader__.load({
602
846
  launcherUrlPlaceholder: "粘贴直链(.zip/.7z/.exe)或 139 分享页链接…",
603
847
  launcherTitlePlaceholder: "标题(留空自动取文件名)",
604
848
  launcherPwdPlaceholder: "解压密码(加密包选填)",
605
- launcherCodePlaceholder: "提取码(139 分享选填)",
849
+ launcherCodePlaceholder: "提取码(139 选填)",
606
850
  launcherPwdNeed: "压缩包已加密,请填写解压密码后重试",
607
851
  launcherPwdWrong: "解压密码错误,或压缩包已损坏",
608
852
  launcherAuthTitle: "139 登录态",
609
853
  launcherAuthHint: "手动方式:登录 yun.139.com 后,F12 → 网络 → 任意请求 → 请求标头里的 Authorization,整串复制粘贴到这里",
610
- launcherHelperLink: "一键方式:安装登录态同步助手",
854
+ launcherHelperLink: "一键方式:安装登录态同步助手(139 网盘)",
611
855
  launcherTutorialBeta: "该功能为测试版本",
612
856
  launcherTutorialPrepTitle: "启动前的准备:",
613
857
  launcherTutorialPrep1: "安装油猴(Tampermonkey 浏览器扩展)",
@@ -635,9 +879,11 @@ window.__ModuleLoader__.load({
635
879
  launcherRootSaved: "✔ 安装位置已更新,之后的安装存到新位置",
636
880
  launcherRootMoved: "✔ 已迁移应用",
637
881
  launcherRootFail: "更改安装位置失败",
638
- launcherShareCode: "该 139 分享需要提取码:请在提取码框填入后重试",
639
- launcherShareCodeWrong: "139 提取码错误,请核对后重试",
640
- launcherShareFail: "139 分享解析失败(详情见括号内服务端信息)",
882
+ launcherShareCode: "该分享需要提取码:请在提取码框填入后重试",
883
+ launcherShareCodeWrong: "提取码错误,请核对后重试",
884
+ launcherShareFail: "分享解析失败(详情见括号内服务端信息)",
885
+ launcherShareCode139: "该 139 分享需要提取码:请在提取码框填入后重试",
886
+ launcherShareCode139Wrong: "139 提取码错误,请核对后重试",
641
887
  launcherInstall: "下载安装",
642
888
  launcherInstalling: "下载安装中…",
643
889
  launcherEmpty: "还没有安装的应用。粘贴直链后点「下载安装」。",
@@ -688,6 +934,10 @@ window.__ModuleLoader__.load({
688
934
  focusIntro: "Background adjusts adaptively to your task",
689
935
  flashFocusOn: "Focus mode on: lens follows mouse (clear center); enable Eye Tracking to follow gaze instead",
690
936
  flashFocusOff: "Focus mode off, manual sliders restored",
937
+ floaterMode: "Desktop Orb",
938
+ flashFloaterOn: "Desktop orb on: when this tab goes to the background or the browser minimizes, a ring button appears on your desktop; click it to return here",
939
+ flashFloaterOff: "Desktop orb off",
940
+ floaterUnsupported: "Desktop orb needs a Windows host with we-floater.exe bundled (unavailable here)",
691
941
  renderModeTitle: "Render Mode",
692
942
  modeEco: "Preview",
693
943
  modePerf: "Capture",
@@ -730,18 +980,39 @@ window.__ModuleLoader__.load({
730
980
  loadFailed: "Failed to load list",
731
981
  openFolderFailed: "Failed to open folder",
732
982
  mountFailed: "Mount failed",
733
- typeDwp: "DWP",
734
- typeWeApp: "WE Apps",
983
+ typeApps: "Apps",
984
+ typeWeApp: "WE app",
735
985
  typeLauncherApp: "App",
986
+ appSubAll: "All",
987
+ manage: "Manage",
988
+ manageDone: "Done",
989
+ openSource: "Open source file",
990
+ detail: "Details",
991
+ detailHide: "Hide",
992
+ detailInstalledAt: "Installed",
993
+ detailAddr: "Location",
994
+ detailExe: "Executable",
995
+ launcherManageHint: "Launch and uninstall from Library → Apps.",
996
+ confirmUninstallTitle: "Uninstall?",
997
+ confirmUninstallBody: "This deletes the local files of (DWP wallpapers can be reinstalled from the market):",
998
+ manageSelected: "Selected",
999
+ bulkUninstall: "Uninstall selected",
1000
+ clearSel: "Clear",
1001
+ manageNotDeletable: "WE Workshop content is not uninstalled here (use \"Open source file\" to locate it)",
1002
+ confirmBulkTitle: (n) => "Uninstall the " + String(n) + " selected item(s)?",
1003
+ flashBulkUninstalled: (n) => "Uninstalled " + String(n) + " item(s)",
1004
+ flashBulkPartial: (n, fail) => "Uninstalled " + String(n) + ", failed: " + fail,
736
1005
  pageSettings: "Settings",
737
1006
  pageLibrary: "Library",
1007
+ pageDwp: "DWP Studio",
738
1008
  pageHint: "Scroll to flip · keep scrolling firmly to turn the page",
739
1009
  pageGapHint: "Keep scrolling to flip · release to bounce back",
740
1010
  mounted: "Mounted",
741
1011
  searchPlaceholder: "Search titles…",
742
1012
  showMore: "Show more",
743
1013
  dwpEmpty: "No installed DWP wallpapers yet — pull some in the library \"Market\" tab.",
744
- weAppEmpty: "No WE application wallpapers.",
1014
+ typeDwp: "DWP",
1015
+ weAppEmpty: "No application wallpapers yet.",
745
1016
  appsCount: (total, matched) => total === matched ? `Total ${String(total)}` : `Total ${String(total)} · Matched ${String(matched)}`,
746
1017
  marketRefresh: "Refresh",
747
1018
  marketSearch: "Search name / author…",
@@ -751,6 +1022,11 @@ window.__ModuleLoader__.load({
751
1022
  marketUpdate: "Update",
752
1023
  marketUninstall: "Uninstall",
753
1024
  marketInstalled: "Installed",
1025
+ marketLocalAhead: "Local is newer",
1026
+ marketLocalAheadHint: "The installed version is newer than the catalog (the remote catalog may not have refreshed yet); downgrading on purpose is not offered here",
1027
+ dwpTierLead: "DWP wallpaper · texture tier: ",
1028
+ dwpTierHd: "high-res (Enhanced)",
1029
+ dwpTierSd: "standard (Eco / Perf)",
754
1030
  marketEmpty: "Catalog is empty",
755
1031
  marketLoading: "Loading…",
756
1032
  marketNoMatch: "No matches",
@@ -780,7 +1056,7 @@ window.__ModuleLoader__.load({
780
1056
  launcherPwdWrong: "Wrong password, or the archive is corrupted",
781
1057
  launcherAuthTitle: "139 Login (Authorization)",
782
1058
  launcherAuthHint: "Manual: sign in at yun.139.com, open DevTools → Network → any request → copy the whole Authorization request header, paste it here",
783
- launcherHelperLink: "One-click: install the login-sync helper",
1059
+ launcherHelperLink: "One-click: install the login-sync helper (139 drive)",
784
1060
  launcherTutorialBeta: "This feature is in beta",
785
1061
  launcherTutorialPrepTitle: "First-time setup:",
786
1062
  launcherTutorialPrep1: "Install Tampermonkey (browser extension)",
@@ -808,9 +1084,11 @@ window.__ModuleLoader__.load({
808
1084
  launcherRootSaved: "✔ Install location updated — future installs go there",
809
1085
  launcherRootMoved: "✔ Apps moved",
810
1086
  launcherRootFail: "Failed to update install location",
811
- launcherShareCode: "This 139 share needs a passcode — enter it in the passcode box and retry",
812
- launcherShareCodeWrong: "Wrong 139 passcode — check it and retry",
813
- launcherShareFail: "139 share resolve failed (see server detail in brackets)",
1087
+ launcherShareCode: "This share needs a passcode — enter it in the passcode box and retry",
1088
+ launcherShareCodeWrong: "Wrong passcode — check it and retry",
1089
+ launcherShareFail: "Share resolve failed (see server detail in brackets)",
1090
+ launcherShareCode139: "This 139 share needs a passcode — enter it in the passcode box and retry",
1091
+ launcherShareCode139Wrong: "Wrong 139 passcode — check it and retry",
814
1092
  launcherInstall: "Download & Install",
815
1093
  launcherInstalling: "Downloading…",
816
1094
  launcherEmpty: "No apps installed yet. Paste a direct link and click \"Download & Install\".",
@@ -866,6 +1144,9 @@ window.__ModuleLoader__.load({
866
1144
  const [gazeError, setGazeError] = (0, react.useState)("");
867
1145
  const [gazeSnapText, setGazeSnapText] = (0, react.useState)(store.settings.gazeSnapText);
868
1146
  const [needsCalib, setNeedsCalib] = (0, react.useState)(false);
1147
+ const [floater, setFloater] = (0, react.useState)(store.settings.floater);
1148
+ /** 能力探测结果(GET /we-sync/floater):false = 非 Windows 或 we-floater.exe 缺失,开关置灰 */
1149
+ const [floaterSupported, setFloaterSupported] = (0, react.useState)(true);
869
1150
  (0, react.useEffect)(() => onGazeStatus((s, err) => {
870
1151
  setGazeStatus(s);
871
1152
  setGazeError(err);
@@ -873,6 +1154,12 @@ window.__ModuleLoader__.load({
873
1154
  const CHARGE_THRESHOLD = 600;
874
1155
  const CHARGE_DECAY_MS = 250;
875
1156
  const PULL_RATIO = .07;
1157
+ const PAGES = [
1158
+ "settings",
1159
+ "library",
1160
+ "dwp"
1161
+ ];
1162
+ const pageIdx = (p) => PAGES.indexOf(p);
876
1163
  const [page, setPage] = (0, react.useState)("settings");
877
1164
  const appsOpen = page === "library";
878
1165
  const panelRef = (0, react.useRef)(null);
@@ -880,9 +1167,16 @@ window.__ModuleLoader__.load({
880
1167
  const trackRef = (0, react.useRef)(null);
881
1168
  const settingsRef = (0, react.useRef)(null);
882
1169
  const gapRef = (0, react.useRef)(null);
883
- const progSetRef = (0, react.useRef)(null);
884
- const progLibRef = (0, react.useRef)(null);
1170
+ const libRef = (0, react.useRef)(null);
1171
+ const gap2Ref = (0, react.useRef)(null);
1172
+ const dwpRef = (0, react.useRef)(null);
1173
+ const progRefs = [
1174
+ (0, react.useRef)(null),
1175
+ (0, react.useRef)(null),
1176
+ (0, react.useRef)(null)
1177
+ ];
885
1178
  const libLoadedRef = (0, react.useRef)(false);
1179
+ const capRef = (0, react.useRef)(null);
886
1180
  const pageRef = (0, react.useRef)("settings");
887
1181
  pageRef.current = page;
888
1182
  const loadLibraryData = () => {
@@ -895,8 +1189,16 @@ window.__ModuleLoader__.load({
895
1189
  };
896
1190
  const geoRef = (0, react.useRef)({
897
1191
  maxPos: 0,
898
- boundary: 0,
899
- libTop: 0
1192
+ tops: [
1193
+ 0,
1194
+ 0,
1195
+ 0
1196
+ ],
1197
+ bottoms: [
1198
+ 0,
1199
+ 0,
1200
+ 0
1201
+ ]
900
1202
  });
901
1203
  const posRef = (0, react.useRef)(0);
902
1204
  const velRef = (0, react.useRef)(0);
@@ -906,25 +1208,76 @@ window.__ModuleLoader__.load({
906
1208
  const phaseRef = (0, react.useRef)("idle");
907
1209
  const animRef = (0, react.useRef)(null);
908
1210
  const decayTimerRef = (0, react.useRef)(null);
1211
+ /** 当前页(翻页动画期间取目标页):决定可用高度——只有 dwp创作页要避让宿主输入框 */
1212
+ const effectivePage = () => {
1213
+ const a = animRef.current;
1214
+ return phaseRef.current === "anim" && a !== null ? a.target : pageRef.current;
1215
+ };
909
1216
  const measure = () => {
910
1217
  const vp = viewportRef.current;
911
1218
  const track = trackRef.current;
912
1219
  const s = settingsRef.current;
913
1220
  const gap = gapRef.current;
914
- if (vp === null || track === null || s === null || gap === null) return;
1221
+ const lib = libRef.current;
1222
+ const gap2 = gap2Ref.current;
1223
+ const dwp = dwpRef.current;
1224
+ if (vp === null || track === null || s === null || gap === null || lib === null || gap2 === null || dwp === null) return;
915
1225
  const vpH = vp.clientHeight;
916
1226
  vp.style.setProperty("--wesync-vph", Math.max(0, vpH) + "px");
917
- const libTop = Math.max(0, s.offsetHeight + gap.offsetHeight);
918
1227
  const maxPos = Math.max(0, track.scrollHeight - vpH);
1228
+ const hs = [
1229
+ s.offsetHeight,
1230
+ lib.offsetHeight,
1231
+ dwp.offsetHeight
1232
+ ];
1233
+ const tops = [
1234
+ 0,
1235
+ hs[0] + gap.offsetHeight,
1236
+ hs[0] + gap.offsetHeight + hs[1] + gap2.offsetHeight
1237
+ ];
1238
+ const bottoms = tops.map((top, i) => i === PAGES.length - 1 ? maxPos : Math.max(top, Math.min(top + hs[i] - vpH, maxPos)));
919
1239
  geoRef.current = {
920
1240
  maxPos,
921
- libTop: Math.min(libTop, maxPos),
922
- boundary: Math.min(Math.max(0, s.offsetHeight - vpH), maxPos)
1241
+ tops,
1242
+ bottoms
923
1243
  };
924
1244
  };
1245
+ const rangeOf = (i) => {
1246
+ const g = geoRef.current;
1247
+ const k = Math.max(0, Math.min(PAGES.length - 1, i));
1248
+ return {
1249
+ top: g.tops[k],
1250
+ bottom: g.bottoms[k]
1251
+ };
1252
+ };
1253
+ /**
1254
+ * 可用高度:面板顶 → 底部边界,写进 --wesync-pages-max。
1255
+ *
1256
+ * 宿主输入框(composer)是 sticky + z-index 7 的常驻条,固定占着会话列底部
1257
+ * (实测 126px,多行输入还会变高)。压在它下面的内容点不到——elementFromPoint
1258
+ * 命中的是输入框的「+」按钮 / 模式行,而不是卡片按钮。
1259
+ * 现在「设置」「壁纸库」两页由 CSS 把输入框整体淡出并放行指针(点得到、看不见),
1260
+ * 只有 dwp创作页显示它,所以也只有那页需要把它的高度让出来。
1261
+ * 其余页用视口底部当边界——顺带修掉原来写死 32px 造成的底部溢出
1262
+ * (实测 1100×700 面板底边超出视口 44px,而宿主滚动被钉住、滚不回来)。
1263
+ */
1264
+ const syncViewportCap = () => {
1265
+ const host = panelRef.current;
1266
+ if (host === null) return;
1267
+ const seat = effectivePage() === "dwp" ? document.querySelector("[data-composer-seat]") : null;
1268
+ const limit = seat !== null ? Math.min(seat.getBoundingClientRect().top, window.innerHeight - 8) : window.innerHeight - 8;
1269
+ let avail = Math.round(limit - host.getBoundingClientRect().top);
1270
+ if (avail < 240) avail = 0;
1271
+ if (avail > 0) host.style.setProperty("--wesync-pages-max", avail + "px");
1272
+ else host.style.removeProperty("--wesync-pages-max");
1273
+ measure();
1274
+ if (phaseRef.current !== "anim") {
1275
+ const r = rangeOf(pageIdx(pageRef.current));
1276
+ posRef.current = Math.max(r.top, Math.min(r.bottom, posRef.current));
1277
+ }
1278
+ };
925
1279
  const resetProgress = () => {
926
- if (progSetRef.current !== null) progSetRef.current.style.transform = "scaleX(0)";
927
- if (progLibRef.current !== null) progLibRef.current.style.transform = "scaleX(0)";
1280
+ for (const p of progRefs) if (p.current !== null) p.current.style.transform = "scaleX(0)";
928
1281
  };
929
1282
  const cancelCharge = () => {
930
1283
  accRef.current = 0;
@@ -939,17 +1292,17 @@ window.__ModuleLoader__.load({
939
1292
  };
940
1293
  /** 蓄力翻页:引擎动画滚过断层到相邻页顶,到达后再切页签高亮 */
941
1294
  const flipTo = (target) => {
942
- const g = geoRef.current;
943
1295
  phaseRef.current = "anim";
944
1296
  cancelCharge();
945
1297
  disarmDecay();
946
- const to = target === "library" ? g.libTop : 0;
947
1298
  animRef.current = {
948
1299
  from: posRef.current,
949
- to,
1300
+ to: posRef.current,
950
1301
  start: performance.now(),
951
1302
  target
952
1303
  };
1304
+ syncViewportCap();
1305
+ animRef.current.to = rangeOf(pageIdx(target)).top;
953
1306
  if (target === "library") loadLibraryData();
954
1307
  };
955
1308
  (0, react.useEffect)(() => {
@@ -1006,13 +1359,12 @@ window.__ModuleLoader__.load({
1006
1359
  if (e.deltaMode === 1) dy *= 40;
1007
1360
  else if (e.deltaMode === 2) dy *= 800;
1008
1361
  if (dy === 0) return;
1009
- const g = geoRef.current;
1362
+ geoRef.current;
1010
1363
  const dir = dy > 0 ? 1 : -1;
1011
- const cur = pageRef.current;
1012
- const pageTop = cur === "library" ? g.libTop : 0;
1013
- const pageBottom = cur === "library" ? g.maxPos : g.boundary;
1364
+ const cur = pageIdx(pageRef.current);
1365
+ const { top: pageTop, bottom: pageBottom } = rangeOf(cur);
1014
1366
  const atBoundary = dir === 1 && posRef.current >= pageBottom - .5 || dir === -1 && posRef.current <= pageTop + .5;
1015
- const canFlip = dir === 1 && cur === "settings" || dir === -1 && cur === "library";
1367
+ const canFlip = dir === 1 && cur < PAGES.length - 1 || dir === -1 && cur > 0;
1016
1368
  if (phaseRef.current === "charge" && pullTargetRef.current !== 0 && (dir === 1 && pullTargetRef.current > 0 || dir === -1 && pullTargetRef.current < 0)) {
1017
1369
  phaseRef.current = "idle";
1018
1370
  cancelCharge();
@@ -1037,12 +1389,12 @@ window.__ModuleLoader__.load({
1037
1389
  const ratio = Math.min(1, Math.abs(accRef.current) / CHARGE_THRESHOLD);
1038
1390
  const maxPull = vp.clientHeight * PULL_RATIO;
1039
1391
  pullTargetRef.current = dir * ratio * maxPull * -1;
1040
- const prog = (dir === 1 ? progLibRef : progSetRef).current;
1041
- if (prog !== null) prog.style.transform = "scaleX(" + ratio.toFixed(3) + ")";
1042
- const other = (dir === 1 ? progSetRef : progLibRef).current;
1043
- if (other !== null) other.style.transform = "scaleX(0)";
1392
+ const prog = progRefs[cur + dir];
1393
+ if (prog !== void 0 && prog.current !== null) prog.current.style.transform = "scaleX(" + ratio.toFixed(3) + ")";
1394
+ const other = progRefs[cur];
1395
+ if (other !== void 0 && other.current !== null) other.current.style.transform = "scaleX(0)";
1044
1396
  if (Math.abs(accRef.current) >= CHARGE_THRESHOLD) {
1045
- flipTo(dir === 1 ? "library" : "settings");
1397
+ flipTo(PAGES[cur + dir]);
1046
1398
  return;
1047
1399
  }
1048
1400
  }
@@ -1056,26 +1408,37 @@ window.__ModuleLoader__.load({
1056
1408
  const host = panelRef.current;
1057
1409
  if (host === null) return;
1058
1410
  host.addEventListener("wheel", onWheel, { passive: false });
1411
+ capRef.current = syncViewportCap;
1412
+ syncViewportCap();
1413
+ const seatEl = document.querySelector("[data-composer-seat]");
1414
+ const seatRo = seatEl !== null ? new ResizeObserver(syncViewportCap) : null;
1415
+ if (seatEl !== null && seatRo !== null) seatRo.observe(seatEl);
1059
1416
  const ro = new ResizeObserver(() => {
1060
1417
  measure();
1061
- const g = geoRef.current;
1062
- posRef.current = Math.max(0, Math.min(g.maxPos, posRef.current));
1418
+ const r = rangeOf(pageIdx(pageRef.current));
1419
+ posRef.current = Math.max(r.top, Math.min(r.bottom, posRef.current));
1063
1420
  });
1064
1421
  if (trackRef.current !== null) ro.observe(trackRef.current);
1065
- window.addEventListener("resize", measure);
1422
+ window.addEventListener("resize", syncViewportCap);
1066
1423
  return () => {
1067
1424
  host.removeEventListener("wheel", onWheel);
1068
1425
  ro.disconnect();
1069
- window.removeEventListener("resize", measure);
1426
+ seatRo?.disconnect();
1427
+ window.removeEventListener("resize", syncViewportCap);
1428
+ host.style.removeProperty("--wesync-pages-max");
1429
+ capRef.current = null;
1070
1430
  };
1071
1431
  }, []);
1432
+ (0, react.useEffect)(() => {
1433
+ capRef.current?.();
1434
+ }, [page]);
1072
1435
  (0, react.useEffect)(() => {
1073
1436
  let raf = 0;
1074
1437
  const tick = () => {
1075
1438
  raf = requestAnimationFrame(tick);
1076
1439
  const track = trackRef.current;
1077
1440
  if (track === null) return;
1078
- const g = geoRef.current;
1441
+ geoRef.current;
1079
1442
  const a = animRef.current;
1080
1443
  if (phaseRef.current === "anim" && a !== null) {
1081
1444
  const t = Math.min(1, (performance.now() - a.start) / 340);
@@ -1093,10 +1456,8 @@ window.__ModuleLoader__.load({
1093
1456
  if (Math.abs(velRef.current) > .4) {
1094
1457
  posRef.current += velRef.current;
1095
1458
  velRef.current *= .9;
1096
- const cur = pageRef.current;
1097
- const pageTop = cur === "library" ? g.libTop : 0;
1098
- const pageBottom = cur === "library" ? g.maxPos : g.boundary;
1099
- posRef.current = Math.max(pageTop, Math.min(pageBottom, posRef.current));
1459
+ const r = rangeOf(pageIdx(pageRef.current));
1460
+ posRef.current = Math.max(r.top, Math.min(r.bottom, posRef.current));
1100
1461
  } else velRef.current = 0;
1101
1462
  pullRef.current += (pullTargetRef.current - pullRef.current) * .22;
1102
1463
  if (Math.abs(pullRef.current) < .4 && pullTargetRef.current === 0) pullRef.current = 0;
@@ -1109,10 +1470,9 @@ window.__ModuleLoader__.load({
1109
1470
  /** 页签点击:引擎动画滚到对应页顶 */
1110
1471
  const scrollToPage = (target) => {
1111
1472
  if (phaseRef.current === "anim") return;
1112
- measure();
1113
1473
  if (target === pageRef.current) {
1114
- const g = geoRef.current;
1115
- const top = target === "library" ? g.libTop : 0;
1474
+ syncViewportCap();
1475
+ const { top } = rangeOf(pageIdx(target));
1116
1476
  phaseRef.current = "anim";
1117
1477
  animRef.current = {
1118
1478
  from: posRef.current,
@@ -1128,6 +1488,13 @@ window.__ModuleLoader__.load({
1128
1488
  const [apps, setApps] = (0, react.useState)([]);
1129
1489
  const [appsCounts, setAppsCounts] = (0, react.useState)({});
1130
1490
  const [typeFilter, setTypeFilter] = (0, react.useState)("dwp");
1491
+ const [appSub, setAppSub] = (0, react.useState)("all");
1492
+ const [manageMode, setManageMode] = (0, react.useState)(false);
1493
+ const [libFlash, setLibFlash] = (0, react.useState)("");
1494
+ /** 管理模式的多选集合:key = 'dwp|<id>' / 'app|<启动器 slug>'(只有可卸载的项能进来) */
1495
+ const [sel, setSel] = (0, react.useState)({});
1496
+ /** 待确认的卸载目标:单卡卸载是 1 项,批量是 N 项,共用同一个确认弹层与执行路径 */
1497
+ const [uninstallFor, setUninstallFor] = (0, react.useState)(null);
1131
1498
  const [search, setSearch] = (0, react.useState)("");
1132
1499
  const [visible, setVisible] = (0, react.useState)(60);
1133
1500
  const [appsError, setAppsError] = (0, react.useState)("");
@@ -1165,6 +1532,22 @@ window.__ModuleLoader__.load({
1165
1532
  const [lEntryFor, setLEntryFor] = (0, react.useState)(null);
1166
1533
  const [lDetailFor, setLDetailFor] = (0, react.useState)(null);
1167
1534
  const [lChoices, setLChoices] = (0, react.useState)({});
1535
+ (0, react.useEffect)(() => {
1536
+ if (lDetailFor === null) return;
1537
+ const raf = requestAnimationFrame(() => {
1538
+ const host = panelRef.current;
1539
+ const vp = viewportRef.current;
1540
+ if (host === null || vp === null) return;
1541
+ const el = Array.from(host.querySelectorAll("[data-wesync-detail]")).find((x) => x.getAttribute("data-wesync-detail") === lDetailFor) ?? null;
1542
+ if (el === null) return;
1543
+ measure();
1544
+ const overflow = el.getBoundingClientRect().bottom - vp.getBoundingClientRect().bottom + 12;
1545
+ if (overflow <= 0) return;
1546
+ const r = rangeOf(pageIdx("library"));
1547
+ posRef.current = Math.max(r.top, Math.min(r.bottom, posRef.current + overflow));
1548
+ });
1549
+ return () => cancelAnimationFrame(raf);
1550
+ }, [lDetailFor]);
1168
1551
  (0, react.useEffect)(() => store.subscribe(() => {
1169
1552
  setInfo(store.info);
1170
1553
  setEnabled(store.settings.enabled);
@@ -1176,11 +1559,26 @@ window.__ModuleLoader__.load({
1176
1559
  setRenderMode(store.settings.renderMode);
1177
1560
  setGazeEnabled(store.settings.gazeEnabled);
1178
1561
  setGazeSnapText(store.settings.gazeSnapText);
1562
+ setFloater(store.settings.floater);
1179
1563
  force((x) => x + 1);
1180
1564
  }), []);
1181
1565
  (0, react.useEffect)(() => {
1182
1566
  loadDirs();
1183
1567
  }, []);
1568
+ (0, react.useEffect)(() => {
1569
+ let dead = false;
1570
+ fetch("/we-sync/floater", { cache: "no-store" }).then((r) => r.json()).then((s) => {
1571
+ if (dead || s.supported !== false) return;
1572
+ setFloaterSupported(false);
1573
+ if (store.settings.floater) {
1574
+ store.settings.floater = false;
1575
+ store.notify();
1576
+ }
1577
+ }).catch(() => {});
1578
+ return () => {
1579
+ dead = true;
1580
+ };
1581
+ }, []);
1184
1582
  const flash = (text) => {
1185
1583
  setStatus(text);
1186
1584
  window.setTimeout(() => setStatus(""), 3500);
@@ -1225,6 +1623,17 @@ window.__ModuleLoader__.load({
1225
1623
  store.actions.applyBackground();
1226
1624
  flash(next ? t.flashFocusOn : t.flashFocusOff);
1227
1625
  };
1626
+ const onFloater = () => {
1627
+ if (!floaterSupported) {
1628
+ flash(t.floaterUnsupported);
1629
+ return;
1630
+ }
1631
+ const next = !store.settings.floater;
1632
+ store.settings.floater = next;
1633
+ setFloater(next);
1634
+ store.actions.syncFloater();
1635
+ flash(next ? t.flashFloaterOn : t.flashFloaterOff);
1636
+ };
1228
1637
  const onRenderMode = (mode) => {
1229
1638
  store.settings.renderMode = mode;
1230
1639
  setRenderMode(mode);
@@ -1274,9 +1683,16 @@ window.__ModuleLoader__.load({
1274
1683
  store.notify();
1275
1684
  };
1276
1685
  const loadDwp = async () => {
1686
+ const f = (url, init) => fetch(url, init);
1687
+ let installed = [];
1688
+ try {
1689
+ installed = await fetchInstalled$1(f);
1690
+ } catch {}
1691
+ let catalog = [];
1692
+ try {
1693
+ catalog = await fetchCatalog(f);
1694
+ } catch {}
1277
1695
  try {
1278
- const f = (url, init) => fetch(url, init);
1279
- const [catalog, installed] = await Promise.all([fetchCatalog(f), fetchInstalled$1(f)]);
1280
1696
  const byId = new Map(catalog.map((e) => [e.id, e]));
1281
1697
  const cards = await Promise.all(installed.map(async (it) => {
1282
1698
  const e = byId.get(it.id);
@@ -1317,12 +1733,12 @@ window.__ModuleLoader__.load({
1317
1733
  if (await store.actions.mountDwp(id)) flash(t.mountHint + id);
1318
1734
  else flash(t.mountFailed);
1319
1735
  };
1320
- const loadMarket = async () => {
1736
+ const loadMarket = async (refresh = false) => {
1321
1737
  setMLoading(true);
1322
1738
  setMError("");
1323
1739
  try {
1324
1740
  const f = (url, init) => fetch(url, init);
1325
- const [catalog, installed] = await Promise.all([fetchCatalog(f), fetchInstalled$1(f)]);
1741
+ const [catalog, installed] = await Promise.all([fetchCatalog(f, void 0, { refresh }), fetchInstalled$1(f)]);
1326
1742
  setMCards(buildCards(catalog, installed));
1327
1743
  } catch (e) {
1328
1744
  setMError(String(e.message ?? e));
@@ -1368,6 +1784,87 @@ window.__ModuleLoader__.load({
1368
1784
  flashM(t.flashMUninstalled);
1369
1785
  } else flashM(t.flashMFailed);
1370
1786
  };
1787
+ const flashLib = (msg) => {
1788
+ setLibFlash(msg);
1789
+ window.setTimeout(() => setLibFlash(""), 3e3);
1790
+ };
1791
+ /** dwp 壁纸的「打开源文件」:node 半在资源管理器里定位 packages/<id>.dwp。 */
1792
+ const onRevealDwp = (id) => {
1793
+ fetch("/we-sync/dwp/market/reveal?id=" + encodeURIComponent(id), { cache: "no-store" }).then((res) => {
1794
+ if (!res.ok) flashLib(t.openFolderFailed);
1795
+ }).catch(() => flashLib(t.openFolderFailed));
1796
+ };
1797
+ /** 确认后的卸载:按列表逐项执行(单选=1 项,多选=N 项);dwp 正挂载先撤背景。 */
1798
+ const onConfirmUninstall = async () => {
1799
+ const targets = uninstallFor;
1800
+ setUninstallFor(null);
1801
+ if (targets === null || targets.length === 0) return;
1802
+ let ok = 0;
1803
+ const failed = [];
1804
+ for (const t of targets) if (t.kind === "dwp") {
1805
+ if (!(await uninstall((url, init) => fetch(url, init), t.id)).ok) {
1806
+ failed.push(t.title);
1807
+ continue;
1808
+ }
1809
+ if (store.settings.dwpMounted === t.id) await store.actions.unmountDwp();
1810
+ ok += 1;
1811
+ } else {
1812
+ if (!(await uninstallApp(t.id, (u) => fetch(u))).ok) {
1813
+ failed.push(t.title);
1814
+ continue;
1815
+ }
1816
+ ok += 1;
1817
+ }
1818
+ loadDwp();
1819
+ loadMarket();
1820
+ loadApps();
1821
+ loadLauncher();
1822
+ setSel({});
1823
+ flashLib(failed.length === 0 ? targets.length === 1 ? t.flashMUninstalled : t.flashBulkUninstalled(ok) : t.flashBulkPartial(ok, failed.join("、")));
1824
+ };
1825
+ const selKey = (kind, id) => kind + "|" + id;
1826
+ const toggleSel = (key) => {
1827
+ setSel((s) => {
1828
+ const n = { ...s };
1829
+ if (n[key] === true) delete n[key];
1830
+ else n[key] = true;
1831
+ return n;
1832
+ });
1833
+ };
1834
+ const selCount = Object.keys(sel).length;
1835
+ /** 选中集 → 卸载目标列表(标题与路径现查,保证与实际安装记录一致)。 */
1836
+ const collectSelTargets = () => {
1837
+ const out = [];
1838
+ for (const key of Object.keys(sel)) {
1839
+ const cut = key.indexOf("|");
1840
+ const kind = key.slice(0, cut);
1841
+ const id = key.slice(cut + 1);
1842
+ if (kind === "dwp") {
1843
+ const card = dwpCards.find((d) => d.id === id);
1844
+ out.push({
1845
+ kind: "dwp",
1846
+ id,
1847
+ title: card !== void 0 ? card.name : id,
1848
+ path: "packages/" + id + ".dwp"
1849
+ });
1850
+ } else {
1851
+ const rec = lApps.find((r) => r.id === id);
1852
+ out.push({
1853
+ kind: "app",
1854
+ id,
1855
+ title: rec !== void 0 ? rec.title : id,
1856
+ path: launcherAppDir(lRoot, rec !== void 0 ? rec.slug : id)
1857
+ });
1858
+ }
1859
+ }
1860
+ return out;
1861
+ };
1862
+ (0, react.useEffect)(() => {
1863
+ if (!manageMode) setSel({});
1864
+ }, [manageMode]);
1865
+ (0, react.useEffect)(() => {
1866
+ setSel({});
1867
+ }, [libTab]);
1371
1868
  const onAppOpen = (id) => {
1372
1869
  fetch("/we-sync/apps/open?id=" + encodeURIComponent(id), { cache: "no-store" }).then((res) => {
1373
1870
  if (!res.ok) flash(t.openFolderFailed);
@@ -1496,7 +1993,8 @@ window.__ModuleLoader__.load({
1496
1993
  }
1497
1994
  if (out.code === "share_passcode_required" || out.code === "share_passcode_wrong") {
1498
1995
  setLCodeErr(true);
1499
- flashLErr(t.flashLFailed + ":" + (out.code === "share_passcode_required" ? t.launcherShareCode : t.launcherShareCodeWrong) + (out.error !== void 0 ? "(" + out.error + ")" : ""));
1996
+ const req = out.code === "share_passcode_required" ? is139Share(url) ? t.launcherShareCode139 : t.launcherShareCode : is139Share(url) ? t.launcherShareCode139Wrong : t.launcherShareCodeWrong;
1997
+ flashLErr(t.flashLFailed + ":" + req + (out.error !== void 0 ? "(" + out.error + ")" : ""));
1500
1998
  return;
1501
1999
  }
1502
2000
  if (out.code === "share_auth_required") {
@@ -1573,14 +2071,6 @@ window.__ModuleLoader__.load({
1573
2071
  if (r.ok) flashL(t.flashLLaunched + ":" + rec.title);
1574
2072
  else flashLErr(t.flashLFailed + (r.error !== void 0 ? ":" + r.error : ""));
1575
2073
  };
1576
- const onLauncherUninstall = async (rec) => {
1577
- const r = await uninstallApp(rec.id, (u) => fetch(u));
1578
- if (r.ok) {
1579
- flashL(t.flashLUninstalled);
1580
- loadLauncher();
1581
- loadApps();
1582
- } else flashL(t.flashLFailed + (r.error !== void 0 ? ":" + r.error : ""));
1583
- };
1584
2074
  const onUpdatePreview = async (rec) => {
1585
2075
  const dataUrl = makeLauncherCard(rec.title);
1586
2076
  if (dataUrl === "") {
@@ -1654,9 +2144,9 @@ window.__ModuleLoader__.load({
1654
2144
  if (appsOpen) loadApps();
1655
2145
  } catch {}
1656
2146
  };
1657
- const weApps = apps.filter((a) => a.type === "application");
2147
+ const appGroups = partitionApps(apps);
1658
2148
  const kw = search.trim().toLowerCase();
1659
- const filteredApps = weApps.filter((a) => kw === "" || a.title.toLowerCase().includes(kw));
2149
+ const filteredApps = appsForSub(apps, appSub).filter((a) => kw === "" || a.title.toLowerCase().includes(kw));
1660
2150
  const filteredDwp = dwpCards.filter((d) => kw === "" || d.name.toLowerCase().includes(kw));
1661
2151
  const shownApps = filteredApps.slice(0, visible);
1662
2152
  const shownDwp = filteredDwp.slice(0, visible);
@@ -1665,7 +2155,7 @@ window.__ModuleLoader__.load({
1665
2155
  const wallpaper = info !== null && info.wallpaper !== null ? info.wallpaper : null;
1666
2156
  const title = wallpaper === null ? info !== null && info.kind === "web" ? t.webNoPreview : t.noWallpaper : wallpaper.title;
1667
2157
  const scene = info !== null && info.source.kind === "scene" ? info.scene : null;
1668
- const subtitle = wallpaper === null ? t.applyHint : scene === null ? "" : renderMode === "eco" ? t.sceneEco : scene.live === true ? t.sceneExternal + " " + String(scene.status?.fps ?? "?") + "fps" : scene.model === true ? t.sceneModel : t.sceneFallback + scene.fallback;
2158
+ const subtitle = store.settings.dwpMounted !== null ? t.dwpTierLead + (renderMode === "enhanced" ? t.dwpTierHd : t.dwpTierSd) : wallpaper === null ? t.applyHint : scene === null ? "" : renderMode === "eco" ? t.sceneEco : scene.live === true ? t.sceneExternal + " " + String(scene.status?.fps ?? "?") + "fps" : scene.model === true ? t.sceneModel : t.sceneFallback + scene.fallback;
1669
2159
  const monitors = info !== null && Array.isArray(info.monitors) && info.monitors.length > 1 ? info.monitors : null;
1670
2160
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1671
2161
  className: "wesync-panel",
@@ -1767,9 +2257,15 @@ window.__ModuleLoader__.load({
1767
2257
  }, m);
1768
2258
  })
1769
2259
  }),
1770
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1771
2261
  className: "wesync-actions",
1772
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2262
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2263
+ className: ["wesync-btn", floater ? "wesync-focusOn" : "wesync-focusOff"].join(" "),
2264
+ onClick: onFloater,
2265
+ disabled: !floaterSupported,
2266
+ title: floaterSupported ? "" : t.floaterUnsupported,
2267
+ children: t.floaterMode
2268
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1773
2269
  className: "wesync-focuswrap",
1774
2270
  onMouseEnter: () => setFocusHover(true),
1775
2271
  onMouseLeave: () => setFocusHover(false),
@@ -1815,7 +2311,7 @@ window.__ModuleLoader__.load({
1815
2311
  children: t.gazeStatusLoading
1816
2312
  }) : null)]
1817
2313
  }) : null]
1818
- })
2314
+ })]
1819
2315
  }),
1820
2316
  focus ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1821
2317
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
@@ -1979,6 +2475,7 @@ window.__ModuleLoader__.load({
1979
2475
  }),
1980
2476
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1981
2477
  className: "wesync-page wesync-page-library",
2478
+ ref: libRef,
1982
2479
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1983
2480
  className: "wesync-card",
1984
2481
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2017,157 +2514,297 @@ window.__ModuleLoader__.load({
2017
2514
  })
2018
2515
  ]
2019
2516
  }),
2020
- libTab === "local" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2021
- className: "wesync-apps-filters",
2022
- children: [
2023
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2024
- className: ["wesync-chip", typeFilter === "dwp" ? "wesync-chip-on" : ""].join(" "),
2025
- onClick: () => {
2026
- setTypeFilter("dwp");
2027
- setVisible(60);
2028
- },
2029
- children: t.typeDwp + " " + String(dwpCards.length)
2030
- }),
2031
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2032
- className: ["wesync-chip", typeFilter === "weapp" ? "wesync-chip-on" : ""].join(" "),
2033
- onClick: () => {
2034
- setTypeFilter("weapp");
2035
- setVisible(60);
2036
- },
2037
- children: t.typeWeApp + " " + String(weApps.length)
2038
- }),
2039
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2040
- className: "wesync-app-search",
2041
- placeholder: t.searchPlaceholder,
2042
- value: search,
2043
- onChange: (e) => {
2044
- setSearch(e.target.value);
2045
- setVisible(60);
2046
- }
2047
- })
2048
- ]
2049
- }), typeFilter === "dwp" ? dwpCards.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2050
- className: "wesync-app-empty",
2051
- children: t.dwpEmpty
2052
- }) : filteredDwp.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2053
- className: "wesync-app-empty",
2054
- children: t.appsNoMatch
2055
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2056
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2057
- className: "wesync-apps-count",
2058
- children: t.appsCount(dwpCards.length, filteredDwp.length)
2059
- }),
2060
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2061
- className: "wesync-apps-grid",
2062
- children: shownDwp.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2063
- className: "wesync-app-card",
2064
- title: (store.settings.dwpMounted === d.id ? t.unmountHint : t.mountHint) + d.name,
2065
- onClick: () => {
2066
- onToggleDwp(d.id);
2067
- },
2068
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2069
- className: "wesync-app-thumbwrap",
2070
- children: [d.thumbnail !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2071
- className: "wesync-app-thumb",
2072
- src: d.thumbnail,
2073
- alt: d.name,
2074
- loading: "lazy",
2075
- onError: (e) => {
2076
- e.currentTarget.style.visibility = "hidden";
2077
- }
2078
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2079
- className: "wesync-app-thumb",
2080
- style: {
2081
- display: "flex",
2082
- alignItems: "center",
2083
- justifyContent: "center"
2084
- },
2085
- children: t.noPreview
2086
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2087
- className: "wesync-app-badge wesync-badge-" + (store.settings.dwpMounted === d.id ? "video" : "image"),
2088
- children: store.settings.dwpMounted === d.id ? t.mounted : t.typeDwp
2089
- })]
2090
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2091
- className: "wesync-app-title",
2092
- children: d.name
2093
- })]
2094
- }, d.id))
2095
- }),
2096
- filteredDwp.length > shownDwp.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2097
- className: "wesync-btn wesync-show-more",
2098
- onClick: () => setVisible((v) => v + 60),
2099
- children: t.showMore + " (+60)"
2100
- }) : null
2101
- ] }) : weApps.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2102
- className: "wesync-app-empty",
2103
- children: t.weAppEmpty
2104
- }) : filteredApps.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2105
- className: "wesync-app-empty",
2106
- children: t.appsNoMatch
2107
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2108
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2109
- className: "wesync-apps-count",
2110
- children: t.appsCount(weApps.length, filteredApps.length)
2517
+ libTab === "local" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2518
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2519
+ className: "wesync-apps-filters",
2520
+ children: [
2521
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2522
+ className: ["wesync-chip", typeFilter === "dwp" ? "wesync-chip-on" : ""].join(" "),
2523
+ onClick: () => {
2524
+ setTypeFilter("dwp");
2525
+ setVisible(60);
2526
+ },
2527
+ children: t.typeDwp + " " + String(dwpCards.length)
2528
+ }),
2529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2530
+ className: ["wesync-chip", typeFilter === "apps" ? "wesync-chip-on" : ""].join(" "),
2531
+ onClick: () => {
2532
+ setTypeFilter("apps");
2533
+ setVisible(60);
2534
+ },
2535
+ children: t.typeApps + " " + String(appGroups.all.length)
2536
+ }),
2537
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2538
+ className: "wesync-app-search",
2539
+ placeholder: t.searchPlaceholder,
2540
+ value: search,
2541
+ onChange: (e) => {
2542
+ setSearch(e.target.value);
2543
+ setVisible(60);
2544
+ }
2545
+ }),
2546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2547
+ className: ["wesync-chip", manageMode ? "wesync-chip-on" : ""].join(" "),
2548
+ title: manageMode ? t.manageDone : t.manage,
2549
+ onClick: () => {
2550
+ setManageMode(!manageMode);
2551
+ },
2552
+ children: manageMode ? t.manageDone : t.manage
2553
+ })
2554
+ ]
2111
2555
  }),
2112
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2113
- className: "wesync-apps-grid",
2114
- children: shownApps.map((app) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2115
- className: "wesync-app-card",
2116
- title: t.openFolder + app.title,
2117
- onClick: () => onAppOpen(app.id),
2118
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2119
- className: "wesync-app-thumbwrap",
2120
- children: [app.hasPreview ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2121
- className: "wesync-app-thumb",
2122
- src: "/we-sync/apps/preview?id=" + encodeURIComponent(app.id),
2123
- alt: app.title,
2124
- loading: "lazy"
2125
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2126
- className: "wesync-app-thumb",
2127
- style: {
2128
- display: "flex",
2129
- alignItems: "center",
2130
- justifyContent: "center"
2131
- },
2132
- children: t.noPreview
2133
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2134
- className: "wesync-app-badge wesync-badge-launcher",
2135
- children: app.source === "launcher" ? t.typeLauncherApp : t.typeWeApp
2136
- })]
2137
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2138
- className: "wesync-app-title",
2139
- style: {
2140
- display: "flex",
2141
- alignItems: "center",
2142
- gap: 6
2556
+ typeFilter === "apps" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2557
+ className: "wesync-apps-filters wesync-apps-subs",
2558
+ children: [
2559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2560
+ className: ["wesync-chip", appSub === "all" ? "wesync-chip-on" : ""].join(" "),
2561
+ onClick: () => {
2562
+ setAppSub("all");
2563
+ setVisible(60);
2143
2564
  },
2144
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2145
- style: {
2146
- flex: 1,
2147
- minWidth: 0,
2148
- overflow: "hidden",
2149
- textOverflow: "ellipsis",
2150
- whiteSpace: "nowrap"
2565
+ children: t.appSubAll + " " + String(appGroups.all.length)
2566
+ }),
2567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2568
+ className: ["wesync-chip", appSub === "we" ? "wesync-chip-on" : ""].join(" "),
2569
+ onClick: () => {
2570
+ setAppSub("we");
2571
+ setVisible(60);
2572
+ },
2573
+ children: t.typeWeApp + " " + String(appGroups.we.length)
2574
+ }),
2575
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2576
+ className: ["wesync-chip", appSub === "launcher" ? "wesync-chip-on" : ""].join(" "),
2577
+ onClick: () => {
2578
+ setAppSub("launcher");
2579
+ setVisible(60);
2580
+ },
2581
+ children: t.typeLauncherApp + " " + String(appGroups.launcher.length)
2582
+ })
2583
+ ]
2584
+ }) : null,
2585
+ libFlash !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2586
+ className: "wesync-market-flash",
2587
+ children: libFlash
2588
+ }) : null,
2589
+ manageMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2590
+ className: "wesync-apps-filters wesync-selbar",
2591
+ children: [
2592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2593
+ className: "wesync-selbar-count",
2594
+ children: [
2595
+ t.manageSelected,
2596
+ " ",
2597
+ String(selCount)
2598
+ ]
2599
+ }),
2600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2601
+ className: "wesync-btn wesync-manage-del",
2602
+ disabled: selCount === 0,
2603
+ onClick: () => setUninstallFor(collectSelTargets()),
2604
+ children: t.bulkUninstall
2605
+ }),
2606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2607
+ className: "wesync-btn",
2608
+ disabled: selCount === 0,
2609
+ onClick: () => setSel({}),
2610
+ children: t.clearSel
2611
+ })
2612
+ ]
2613
+ }) : null,
2614
+ typeFilter === "dwp" ? dwpCards.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2615
+ className: "wesync-app-empty",
2616
+ children: t.dwpEmpty
2617
+ }) : filteredDwp.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2618
+ className: "wesync-app-empty",
2619
+ children: t.appsNoMatch
2620
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2622
+ className: "wesync-apps-count",
2623
+ children: t.appsCount(dwpCards.length, filteredDwp.length)
2624
+ }),
2625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2626
+ className: "wesync-apps-grid",
2627
+ children: shownDwp.map((d, di) => {
2628
+ const key = selKey("dwp", d.id);
2629
+ const picked = sel[key] === true;
2630
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2631
+ className: [
2632
+ "wesync-app-card",
2633
+ manageMode ? "wesync-app-card-manage" : "",
2634
+ picked ? "wesync-app-card-selected" : ""
2635
+ ].join(" "),
2636
+ style: manageMode ? { animationDelay: (-.24 * (di % 5)).toFixed(2) + "s" } : void 0,
2637
+ title: manageMode ? d.name : (store.settings.dwpMounted === d.id ? t.unmountHint : t.mountHint) + d.name,
2638
+ onClick: manageMode ? () => {
2639
+ toggleSel(key);
2640
+ } : () => {
2641
+ onToggleDwp(d.id);
2151
2642
  },
2152
- children: app.title
2153
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2154
- className: "wesync-btn wesync-app-launch",
2155
- title: t.launcherLaunch,
2156
- onClick: (e) => {
2157
- e.stopPropagation();
2643
+ children: [
2644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2645
+ className: "wesync-app-thumbwrap",
2646
+ children: [
2647
+ d.thumbnail !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2648
+ className: "wesync-app-thumb",
2649
+ src: d.thumbnail,
2650
+ alt: d.name,
2651
+ loading: "lazy",
2652
+ onError: (e) => {
2653
+ e.currentTarget.style.visibility = "hidden";
2654
+ }
2655
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2656
+ className: "wesync-app-thumb",
2657
+ style: {
2658
+ display: "flex",
2659
+ alignItems: "center",
2660
+ justifyContent: "center"
2661
+ },
2662
+ children: t.noPreview
2663
+ }),
2664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2665
+ className: "wesync-app-badge wesync-badge-" + (store.settings.dwpMounted === d.id ? "video" : "image"),
2666
+ children: store.settings.dwpMounted === d.id ? t.mounted : t.typeDwp
2667
+ }),
2668
+ picked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2669
+ className: "wesync-app-selmark",
2670
+ children: "✓"
2671
+ }) : null
2672
+ ]
2673
+ }),
2674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2675
+ className: "wesync-app-title",
2676
+ children: d.name
2677
+ }),
2678
+ manageMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2679
+ className: "wesync-manage-row",
2680
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2681
+ className: "wesync-btn",
2682
+ onClick: (e) => {
2683
+ e.stopPropagation();
2684
+ onRevealDwp(d.id);
2685
+ },
2686
+ children: t.openSource
2687
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2688
+ className: "wesync-btn wesync-manage-del",
2689
+ onClick: (e) => {
2690
+ e.stopPropagation();
2691
+ setUninstallFor([{
2692
+ kind: "dwp",
2693
+ id: d.id,
2694
+ title: d.name,
2695
+ path: "packages/" + d.id + ".dwp"
2696
+ }]);
2697
+ },
2698
+ children: t.marketUninstall
2699
+ })]
2700
+ }) : null
2701
+ ]
2702
+ }, d.id);
2703
+ })
2704
+ }),
2705
+ filteredDwp.length > shownDwp.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2706
+ className: "wesync-btn wesync-show-more",
2707
+ onClick: () => setVisible((v) => v + 60),
2708
+ children: t.showMore + " (+60)"
2709
+ }) : null
2710
+ ] }) : appGroups.all.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2711
+ className: "wesync-app-empty",
2712
+ children: t.weAppEmpty
2713
+ }) : filteredApps.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2714
+ className: "wesync-app-empty",
2715
+ children: t.appsNoMatch
2716
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2718
+ className: "wesync-apps-count",
2719
+ children: t.appsCount(appGroups.all.length, filteredApps.length)
2720
+ }),
2721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2722
+ className: "wesync-apps-grid",
2723
+ children: shownApps.map((app, ai) => {
2724
+ const rec = matchLauncherRecord(app, lApps);
2725
+ const isLauncher = (app.source ?? "") === "launcher";
2726
+ const key = rec !== null ? selKey("app", rec.id) : "";
2727
+ const picked = rec !== null && sel[key] === true;
2728
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2729
+ className: [
2730
+ "wesync-app-card",
2731
+ manageMode ? "wesync-app-card-manage" : "",
2732
+ picked ? "wesync-app-card-selected" : ""
2733
+ ].join(" "),
2734
+ style: manageMode ? { animationDelay: (-.24 * (ai % 5)).toFixed(2) + "s" } : void 0,
2735
+ title: manageMode ? app.title : t.launcherLaunch + app.title,
2736
+ onClick: manageMode ? () => {
2737
+ if (rec === null) flashLib(t.manageNotDeletable);
2738
+ else toggleSel(key);
2739
+ } : () => {
2158
2740
  onLaunch(app.id, app.title, app.file, null);
2159
2741
  },
2160
- children: t.launcherLaunch
2161
- })]
2162
- })]
2163
- }, app.id))
2164
- }),
2165
- filteredApps.length > shownApps.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2166
- className: "wesync-btn wesync-show-more",
2167
- onClick: () => setVisible((v) => v + 60),
2168
- children: t.showMore + " (+60)"
2169
- }) : null
2170
- ] })] }) : libTab === "market" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2742
+ children: [
2743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2744
+ className: "wesync-app-thumbwrap",
2745
+ children: [
2746
+ app.hasPreview ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2747
+ className: "wesync-app-thumb",
2748
+ src: "/we-sync/apps/preview?id=" + encodeURIComponent(app.id),
2749
+ alt: app.title,
2750
+ loading: "lazy"
2751
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2752
+ className: "wesync-app-thumb",
2753
+ style: {
2754
+ display: "flex",
2755
+ alignItems: "center",
2756
+ justifyContent: "center"
2757
+ },
2758
+ children: t.noPreview
2759
+ }),
2760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2761
+ className: "wesync-app-badge " + (isLauncher ? "wesync-badge-launcher" : "wesync-badge-application"),
2762
+ children: isLauncher ? t.typeLauncherApp : t.typeWeApp
2763
+ }),
2764
+ picked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2765
+ className: "wesync-app-selmark",
2766
+ children: "✓"
2767
+ }) : null
2768
+ ]
2769
+ }),
2770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2771
+ className: "wesync-app-title",
2772
+ children: app.title
2773
+ }),
2774
+ manageMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2775
+ className: "wesync-manage-row",
2776
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2777
+ className: "wesync-btn",
2778
+ onClick: (e) => {
2779
+ e.stopPropagation();
2780
+ onAppOpen(app.id);
2781
+ },
2782
+ children: t.openSource
2783
+ }), rec === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2784
+ className: "wesync-btn wesync-manage-del",
2785
+ onClick: (e) => {
2786
+ e.stopPropagation();
2787
+ setUninstallFor([{
2788
+ kind: "app",
2789
+ id: rec.id,
2790
+ title: app.title,
2791
+ path: launcherAppDir(lRoot, rec.slug)
2792
+ }]);
2793
+ },
2794
+ children: t.marketUninstall
2795
+ })]
2796
+ }) : null
2797
+ ]
2798
+ }, app.id);
2799
+ })
2800
+ }),
2801
+ filteredApps.length > shownApps.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2802
+ className: "wesync-btn wesync-show-more",
2803
+ onClick: () => setVisible((v) => v + 60),
2804
+ children: t.showMore + " (+60)"
2805
+ }) : null
2806
+ ] })
2807
+ ] }) : libTab === "market" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2171
2808
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2172
2809
  className: "wesync-apps-filters",
2173
2810
  children: [
@@ -2190,7 +2827,7 @@ window.__ModuleLoader__.load({
2190
2827
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2191
2828
  className: "wesync-btn",
2192
2829
  onClick: () => {
2193
- loadMarket();
2830
+ loadMarket(true);
2194
2831
  },
2195
2832
  children: t.marketRefresh
2196
2833
  })
@@ -2251,21 +2888,38 @@ window.__ModuleLoader__.load({
2251
2888
  }),
2252
2889
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2253
2890
  className: "wesync-market-actions",
2254
- children: [c.state === "absent" || c.state === "update" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2255
- className: "wesync-btn wesync-market-install",
2256
- disabled: busyId,
2257
- onClick: () => {
2258
- mInstall(c.entry.id, c.state === "update");
2259
- },
2260
- children: busyId ? t.marketInstalling : c.state === "update" ? t.marketUpdate : t.marketInstall
2261
- }) : null, c.state === "installed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2262
- className: "wesync-btn wesync-market-uninstall",
2263
- disabled: busyId,
2264
- onClick: () => {
2265
- mUninstall(c.entry.id);
2266
- },
2267
- children: t.marketUninstall
2268
- }) : null]
2891
+ children: [
2892
+ c.state === "absent" || c.state === "update" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2893
+ className: "wesync-btn wesync-market-install",
2894
+ disabled: busyId,
2895
+ onClick: () => {
2896
+ mInstall(c.entry.id, c.state === "update");
2897
+ },
2898
+ children: busyId ? t.marketInstalling : c.state === "update" ? t.marketUpdate : t.marketInstall
2899
+ }) : null,
2900
+ c.state === "local-ahead" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2901
+ className: "wesync-btn",
2902
+ disabled: true,
2903
+ title: t.marketLocalAheadHint,
2904
+ children: t.marketLocalAhead
2905
+ }) : null,
2906
+ c.state === "installed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2907
+ className: "wesync-btn wesync-market-uninstall",
2908
+ disabled: busyId,
2909
+ onClick: () => {
2910
+ mUninstall(c.entry.id);
2911
+ },
2912
+ children: t.marketUninstall
2913
+ }) : null,
2914
+ c.state === "local-ahead" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2915
+ className: "wesync-btn wesync-market-uninstall",
2916
+ disabled: busyId,
2917
+ onClick: () => {
2918
+ mUninstall(c.entry.id);
2919
+ },
2920
+ children: t.marketUninstall
2921
+ }) : null
2922
+ ]
2269
2923
  })
2270
2924
  ]
2271
2925
  }, c.entry.id);
@@ -2414,7 +3068,7 @@ window.__ModuleLoader__.load({
2414
3068
  "3. ",
2415
3069
  t.launcherTutorialPrep3Lead,
2416
3070
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2417
- href: "/we-sync/139-helper.user.js",
3071
+ href: "/we-sync/login-sync.user.js",
2418
3072
  target: "_blank",
2419
3073
  rel: "noreferrer",
2420
3074
  style: {
@@ -2452,129 +3106,161 @@ window.__ModuleLoader__.load({
2452
3106
  }) : (() => {
2453
3107
  const kwL = lSearch.trim().toLowerCase();
2454
3108
  const filteredL = lApps.filter((a) => kwL === "" || a.title.toLowerCase().includes(kwL));
2455
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2456
- className: "wesync-apps-filters",
2457
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2458
- className: "wesync-app-search",
2459
- placeholder: t.searchPlaceholder,
2460
- value: lSearch,
2461
- onChange: (e) => setLSearch(e.target.value)
2462
- })
2463
- }), filteredL.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2464
- className: "wesync-app-empty",
2465
- children: t.launcherNoMatch
2466
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2467
- className: "wesync-apps-grid",
2468
- children: filteredL.map((rec) => {
2469
- const candidates = lChoices[rec.id] ?? [];
2470
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2471
- className: "wesync-app-card wesync-market-card",
2472
- children: [
2473
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2474
- className: "wesync-app-thumbwrap",
2475
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2476
- className: "wesync-app-thumb",
2477
- src: "/we-sync/launcher/preview-file?id=" + encodeURIComponent(rec.id),
2478
- alt: rec.title,
2479
- loading: "lazy",
2480
- onError: (e) => {
2481
- e.currentTarget.style.visibility = "hidden";
2482
- }
2483
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2484
- className: "wesync-app-badge wesync-badge-launcher",
2485
- children: t.typeLauncherApp
2486
- })]
2487
- }),
2488
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2489
- className: "wesync-app-title",
2490
- children: rec.title
2491
- }),
2492
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2493
- className: "wesync-market-meta",
2494
- children: [
2495
- humanSize(rec.size),
2496
- " · ",
2497
- rec.file
2498
- ]
2499
- }),
2500
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2501
- className: "wesync-market-actions",
2502
- children: [
2503
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2504
- className: "wesync-btn wesync-market-install",
2505
- onClick: () => onLaunch(rec.id, rec.title, rec.file, rec),
2506
- children: t.launcherLaunch
2507
- }),
2508
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2509
- className: "wesync-btn",
3109
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3111
+ className: "wesync-apps-filters",
3112
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3113
+ className: "wesync-app-search",
3114
+ placeholder: t.searchPlaceholder,
3115
+ value: lSearch,
3116
+ onChange: (e) => setLSearch(e.target.value)
3117
+ })
3118
+ }),
3119
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3120
+ className: "wesync-market-meta",
3121
+ children: t.launcherManageHint
3122
+ }),
3123
+ filteredL.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3124
+ className: "wesync-app-empty",
3125
+ children: t.launcherNoMatch
3126
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3127
+ className: "wesync-apps-grid",
3128
+ children: filteredL.map((rec) => {
3129
+ const candidates = lChoices[rec.id] ?? [];
3130
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3131
+ className: "wesync-app-card wesync-market-card",
3132
+ children: [
3133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3134
+ className: "wesync-app-thumbwrap",
3135
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3136
+ className: "wesync-app-thumb",
3137
+ src: "/we-sync/launcher/preview-file?id=" + encodeURIComponent(rec.id),
3138
+ alt: rec.title,
3139
+ loading: "lazy",
3140
+ onError: (e) => {
3141
+ e.currentTarget.style.visibility = "hidden";
3142
+ }
3143
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3144
+ className: "wesync-app-badge wesync-badge-launcher",
3145
+ children: t.typeLauncherApp
3146
+ })]
3147
+ }),
3148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3149
+ className: "wesync-app-title",
3150
+ children: rec.title
3151
+ }),
3152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3153
+ className: "wesync-market-meta",
3154
+ children: [
3155
+ humanSize(rec.size),
3156
+ " · ",
3157
+ rec.file
3158
+ ]
3159
+ }),
3160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3161
+ className: "wesync-market-actions",
3162
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3163
+ className: "wesync-btn wesync-detail-btn",
2510
3164
  onClick: () => {
2511
3165
  setLDetailFor(lDetailFor === rec.id ? null : rec.id);
2512
3166
  },
2513
- children: "…"
2514
- }),
2515
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2516
- className: "wesync-btn wesync-market-uninstall",
2517
- onClick: () => {
2518
- onLauncherUninstall(rec);
2519
- },
2520
- children: t.launcherUninstall
3167
+ children: lDetailFor === rec.id ? t.detailHide : t.detail
2521
3168
  })
2522
- ]
2523
- }),
2524
- lDetailFor === rec.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2525
- className: "wesync-market-meta",
2526
- style: { width: "100%" },
2527
- children: [
2528
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
2529
- t.launcherSource,
2530
- ": ",
2531
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2532
- style: { wordBreak: "break-all" },
2533
- children: rec.sourceUrl
2534
- })
2535
- ] }),
2536
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
2537
- t.launcherSha,
2538
- ": ",
2539
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2540
- style: { wordBreak: "break-all" },
2541
- children: [rec.sha512.slice(0, 32), "…"]
2542
- })
2543
- ] }),
2544
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2545
- className: "wesync-btn",
2546
- style: { marginTop: 4 },
2547
- onClick: () => {
2548
- onUpdatePreview(rec);
2549
- },
2550
- children: t.launcherUpdatePreview
2551
- }),
2552
- candidates.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2553
- style: { marginTop: 6 },
2554
- children: t.launcherCandidates
2555
- }), candidates.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2556
- className: "wesync-btn",
2557
- style: { margin: "2px 4px 0 0" },
2558
- onClick: () => {
2559
- onSetEntry(rec, f);
2560
- },
2561
- children: [
2562
- f === rec.file ? "● " : "",
2563
- f,
2564
- f === rec.file ? "" : " → " + t.launcherSetEntry
2565
- ]
2566
- }, f))] }) : null
2567
- ]
2568
- }) : null
2569
- ]
2570
- }, rec.id);
3169
+ }),
3170
+ lDetailFor === rec.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3171
+ className: "wesync-market-meta",
3172
+ "data-wesync-detail": rec.id,
3173
+ style: { width: "100%" },
3174
+ children: [
3175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
3176
+ t.detailInstalledAt,
3177
+ ": ",
3178
+ formatInstalledAt(rec.installedAt)
3179
+ ] }),
3180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
3181
+ t.detailAddr,
3182
+ ": ",
3183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3184
+ style: { wordBreak: "break-all" },
3185
+ children: launcherAppDir(lRoot, rec.slug)
3186
+ })
3187
+ ] }),
3188
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
3189
+ t.detailExe,
3190
+ ": ",
3191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3192
+ style: { wordBreak: "break-all" },
3193
+ children: rec.file
3194
+ })
3195
+ ] }),
3196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
3197
+ t.launcherSource,
3198
+ ": ",
3199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3200
+ style: { wordBreak: "break-all" },
3201
+ children: rec.sourceUrl
3202
+ })
3203
+ ] }),
3204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
3205
+ t.launcherSha,
3206
+ ": ",
3207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3208
+ style: { wordBreak: "break-all" },
3209
+ children: [rec.sha512.slice(0, 32), "…"]
3210
+ })
3211
+ ] }),
3212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3213
+ className: "wesync-btn",
3214
+ style: { marginTop: 4 },
3215
+ onClick: () => {
3216
+ onUpdatePreview(rec);
3217
+ },
3218
+ children: t.launcherUpdatePreview
3219
+ }),
3220
+ candidates.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3221
+ style: { marginTop: 6 },
3222
+ children: t.launcherCandidates
3223
+ }), candidates.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3224
+ className: "wesync-btn",
3225
+ style: { margin: "2px 4px 0 0" },
3226
+ onClick: () => {
3227
+ onSetEntry(rec, f);
3228
+ },
3229
+ children: [
3230
+ f === rec.file ? "● " : "",
3231
+ f,
3232
+ f === rec.file ? "" : " → " + t.launcherSetEntry
3233
+ ]
3234
+ }, f))] }) : null
3235
+ ]
3236
+ }) : null
3237
+ ]
3238
+ }, rec.id);
3239
+ })
2571
3240
  })
2572
- })] });
3241
+ ] });
2573
3242
  })()
2574
3243
  ] })
2575
3244
  ]
2576
3245
  })
2577
3246
  })
3247
+ }),
3248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3249
+ className: "wesync-page-gap",
3250
+ ref: gap2Ref,
3251
+ children: [
3252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "wesync-page-gap-line" }),
3253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3254
+ className: "wesync-page-hint",
3255
+ children: t.pageGapHint
3256
+ }),
3257
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "wesync-page-gap-line" })
3258
+ ]
3259
+ }),
3260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3261
+ className: "wesync-page wesync-page-dwp",
3262
+ ref: dwpRef,
3263
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DwpEditorCard, {})
2578
3264
  })
2579
3265
  ]
2580
3266
  })
@@ -2616,31 +3302,71 @@ window.__ModuleLoader__.load({
2616
3302
  ]
2617
3303
  })
2618
3304
  }) : null,
2619
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3305
+ uninstallFor !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3306
+ className: "wesync-confirm-mask",
3307
+ onClick: () => setUninstallFor(null),
3308
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3309
+ className: "wesync-confirm",
3310
+ onClick: (e) => {
3311
+ e.stopPropagation();
3312
+ },
3313
+ children: [
3314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3315
+ className: "wesync-confirm-title",
3316
+ children: uninstallFor.length === 1 ? t.confirmUninstallTitle : t.confirmBulkTitle(uninstallFor.length)
3317
+ }),
3318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3319
+ className: "wesync-confirm-body",
3320
+ children: [
3321
+ t.confirmUninstallBody,
3322
+ uninstallFor.slice(0, 8).map((x) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
3323
+ className: "wesync-confirm-path",
3324
+ children: x.title
3325
+ }, x.kind + x.id)),
3326
+ uninstallFor.length === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
3327
+ className: "wesync-confirm-path",
3328
+ children: uninstallFor[0].path
3329
+ }) : null,
3330
+ uninstallFor.length > 8 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3331
+ style: {
3332
+ marginTop: 4,
3333
+ opacity: .75
3334
+ },
3335
+ children: "… +" + String(uninstallFor.length - 8)
3336
+ }) : null
3337
+ ]
3338
+ }),
3339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3340
+ className: "wesync-confirm-actions",
3341
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3342
+ className: "wesync-btn",
3343
+ onClick: () => setUninstallFor(null),
3344
+ children: t.launcherConfirmCancel
3345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3346
+ className: "wesync-btn wesync-market-uninstall",
3347
+ onClick: () => {
3348
+ onConfirmUninstall();
3349
+ },
3350
+ children: t.marketUninstall
3351
+ })]
3352
+ })
3353
+ ]
3354
+ })
3355
+ }) : null,
3356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2620
3357
  className: "wesync-pager",
2621
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2622
- className: ["wesync-pager-dot", page === "settings" ? "wesync-pager-dot-on" : ""].join(" "),
2623
- title: t.pageSettings,
2624
- onClick: () => scrollToPage("settings"),
3358
+ children: PAGES.map((p, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3359
+ className: ["wesync-pager-dot", page === p ? "wesync-pager-dot-on" : ""].join(" "),
3360
+ title: p === "settings" ? t.pageSettings : p === "library" ? t.pageLibrary : t.pageDwp,
3361
+ onClick: () => scrollToPage(p),
2625
3362
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2626
3363
  className: "wesync-pager-label",
2627
- children: t.pageSettings
3364
+ children: p === "settings" ? t.pageSettings : p === "library" ? t.pageLibrary : t.pageDwp
2628
3365
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2629
3366
  className: "wesync-pager-progress",
2630
- ref: progSetRef
3367
+ ref: progRefs[i]
2631
3368
  })]
2632
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2633
- className: ["wesync-pager-dot", page === "library" ? "wesync-pager-dot-on" : ""].join(" "),
2634
- title: t.pageLibrary,
2635
- onClick: () => scrollToPage("library"),
2636
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2637
- className: "wesync-pager-label",
2638
- children: t.pageLibrary
2639
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2640
- className: "wesync-pager-progress",
2641
- ref: progLibRef
2642
- })]
2643
- })]
3369
+ }, p))
2644
3370
  })
2645
3371
  ]
2646
3372
  });
@@ -2707,7 +3433,11 @@ window.__ModuleLoader__.load({
2707
3433
  min-width: 0;
2708
3434
  min-height: 0;
2709
3435
  height: 100%;
2710
- max-height: min(100vh - 32px, 980px);
3436
+ /* 高度上限:默认给宿主头部留 32px、再封 980px。
3437
+ --wesync-pages-max 由面板实测覆盖(面板顶 → 宿主输入框顶):输入框是
3438
+ sticky + z-index 7 的常驻条,压在它下面的内容点不到(elementFromPoint
3439
+ 命中的是输入框),所以滚动视口必须止于输入框上沿。 */
3440
+ max-height: var(--wesync-pages-max, min(100vh - 32px, 980px));
2711
3441
  overflow: hidden;
2712
3442
  position: relative;
2713
3443
  }
@@ -3197,6 +3927,107 @@ body[data-ds-dark-theme] .wesync-gaze-status.is-error { color: #fdba74; }
3197
3927
  padding: 10px 2px;
3198
3928
  }
3199
3929
 
3930
+ /* 壁纸库「本地」栏:应用大类下的二级分类行(全部 / we应用 / 应用)。
3931
+ 与上一行筛选 chips 分开一点点间距,视觉上从属而非并列。 */
3932
+ .wesync-apps-subs {
3933
+ margin: 6px 0 8px;
3934
+ }
3935
+
3936
+ /* 应用启动器卡片:操作行只剩「详细」,撑满整行——点击区域大,
3937
+ 不再是一排挤在卡片底部、字号 11px 的小按钮(原先「启动 / … / 卸载」三个)。 */
3938
+ .wesync-detail-btn {
3939
+ flex: 1;
3940
+ font-size: 12px;
3941
+ padding: 6px 10px;
3942
+ }
3943
+
3944
+ /* 壁纸库「本地」栏 · 管理模式:卡片上的操作行(打开源文件 / 卸载)。
3945
+ 只在「管理」开启时渲染,平时卡片上没有任何小按钮。 */
3946
+ .wesync-manage-row {
3947
+ display: flex;
3948
+ gap: 6px;
3949
+ margin-top: 6px;
3950
+ }
3951
+
3952
+ .wesync-manage-row .wesync-btn {
3953
+ flex: 1;
3954
+ font-size: 11px;
3955
+ padding: 4px 8px;
3956
+ }
3957
+
3958
+ /* 卸载是破坏性操作:与「打开源文件」拉开颜色差异 */
3959
+ .wesync-manage-del {
3960
+ color: #fca5a5;
3961
+ }
3962
+
3963
+ /* 壁纸库「本地」栏 · 管理模式:卡片进入"编辑态"——
3964
+ 整体轻微晃动(iOS 桌面编辑态的观感,每张卡由内联 animation-delay 错开相位),
3965
+ 点卡片 = 多选:选中项固定不再晃,背景转蓝并打勾。
3966
+ 只有真正可卸载的项(dwp 壁纸 / 启动器装的应用)能选中,工坊内容点了给提示。 */
3967
+ @keyframes wesync-jiggle {
3968
+ 0% { transform: rotate(-0.9deg); }
3969
+ 50% { transform: rotate(0.9deg); }
3970
+ 100% { transform: rotate(-0.9deg); }
3971
+ }
3972
+
3973
+ .wesync-app-card-manage {
3974
+ cursor: pointer;
3975
+ /* 0.72s = 原 0.36s 放慢 50%(太快会显得躁,慢一点更像"编辑态待命") */
3976
+ animation: wesync-jiggle 0.72s ease-in-out infinite;
3977
+ }
3978
+
3979
+ /* 选中:停住晃动 + 蓝色高亮(用户要求的"被固定、背景变蓝") */
3980
+ .wesync-app-card-manage.wesync-app-card-selected {
3981
+ animation: none;
3982
+ transform: none;
3983
+ background: rgba(59, 130, 246, 0.26);
3984
+ border-color: rgba(59, 130, 246, 0.72);
3985
+ box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.45);
3986
+ }
3987
+
3988
+ .wesync-app-card-selected:hover {
3989
+ background: rgba(59, 130, 246, 0.32);
3990
+ }
3991
+
3992
+ /* 选中角标:缩略图右上角的勾 */
3993
+ .wesync-app-selmark {
3994
+ position: absolute;
3995
+ top: 6px;
3996
+ right: 6px;
3997
+ width: 18px;
3998
+ height: 18px;
3999
+ border-radius: 50%;
4000
+ display: flex;
4001
+ align-items: center;
4002
+ justify-content: center;
4003
+ font-size: 11px;
4004
+ line-height: 1;
4005
+ color: #fff;
4006
+ background: rgba(59, 130, 246, 0.95);
4007
+ pointer-events: none;
4008
+ }
4009
+
4010
+ /* 管理模式的选择计数条:已选 N + 卸载选中 + 清空选择 */
4011
+ .wesync-selbar {
4012
+ margin: 6px 0 8px;
4013
+ }
4014
+
4015
+ .wesync-selbar-count {
4016
+ font-size: 12px;
4017
+ color: var(--dsw-alias-label-primary);
4018
+ margin-right: 2px;
4019
+ }
4020
+
4021
+ .wesync-selbar .wesync-btn {
4022
+ font-size: 12px;
4023
+ padding: 4px 10px;
4024
+ }
4025
+
4026
+ /* 无障碍:系统开启「减少动态效果」时不晃,选中态仍然清晰 */
4027
+ @media (prefers-reduced-motion: reduce) {
4028
+ .wesync-app-card-manage { animation: none; }
4029
+ }
4030
+
3200
4031
  /* 壁纸库:本地 / 市场 分类行 + 类型筛选 chips + 标题搜索 */
3201
4032
  .wesync-apps-cats {
3202
4033
  display: flex;
@@ -3414,15 +4245,6 @@ body[data-ds-dark-theme] .wesync-gaze-status.is-error { color: #fdba74; }
3414
4245
  .wesync-confirm-actions { display: flex; gap: 8px; justify-content: flex-end; }
3415
4246
  .wesync-confirm-actions .wesync-market-install { flex: 0 0 auto; padding: 6px 18px; }
3416
4247
 
3417
- /* ── 应用启动器:本地库 we应用 瓷砖的 ▶ 启动按钮 ──────────────────── */
3418
- .wesync-app-launch {
3419
- flex: 0 0 auto;
3420
- font-size: 11px;
3421
- padding: 2px 8px;
3422
- border-radius: 999px;
3423
- color: #fca5a5;
3424
- }
3425
-
3426
4248
  /* 禁用会话正文两侧的「拖拽调整宽度」把手,与轨迹页表现一致。
3427
4249
  harness 只在检测到 composer-overlay 标记时隐藏这对把手(见
3428
4250
  ui-conversation ConversationRoot.module.css),而本面板不接管
@@ -3433,14 +4255,18 @@ body:has(.wesync-panel) [data-width-handle] {
3433
4255
  display: none;
3434
4256
  }
3435
4257
 
3436
- /* ── 「设置」页收起宿主输入框 ─────────────────────────────────────────
3437
- share 面板自带全套交互控件,宿主输入框在设置页只会挡住面板底部行;
3438
- 翻到「壁纸库」(或切走标签页)自动恢复。page 由 WallpaperSharePanel
3439
- 随 state 反射到 body[data-wesync-page],属性消失即还原。
3440
- 收起姿势与沉浸模式同款(opacity+pointer-events,不改布局,seat 高度
3441
- 变量稳定)。:has([data-composer-card]) 只认真正的输入条:授权/提问/
3442
- 子代理只读条接管编辑器期间 seat 保持可见,不会吞掉等待回答的面板。 */
3443
- body[data-wesync-page='settings'] [data-composer-seat]:has([data-composer-card]) {
4258
+ /* ── 「设置」「壁纸库」全程收起宿主输入框,只有「dwp创作」页显示 ────────
4259
+ 两层原因:
4260
+ 1) 观感:输入框固定占着会话列底部(实测 126px),两页的卡片底部行被它压住;
4261
+ 2) 可点性:它是 sticky + z-index 7 的常驻条,压在它下面的控件点不到——
4262
+ elementFromPoint 命中的是输入框的「+」按钮 / 模式行,而不是卡片按钮
4263
+ (实测 1280×800:应用启动器卡片的按钮点击全被输入框吞掉)。
4264
+ 收起姿势与沉浸模式同款(opacity + pointer-events,不改布局、seat 高度稳定):
4265
+ 看不见,但点击穿透到面板。dwp创作页(唯一需要输入框的页)不加这条规则,
4266
+ 其可用高度由面板实测的 --wesync-pages-max 扣掉输入框高度。
4267
+ :has([data-composer-card]) 只认真正的输入条:授权/提问/子代理只读条
4268
+ 接管编辑器期间 seat 保持可见,不会把等待回答的输入框一起藏掉。 */
4269
+ body[data-wesync-page]:not([data-wesync-page='dwp']) [data-composer-seat]:has([data-composer-card]) {
3444
4270
  opacity: 0 !important;
3445
4271
  pointer-events: none !important;
3446
4272
  transition: opacity 0.3s ease !important;
@@ -7088,6 +7914,7 @@ void main() {
7088
7914
  }
7089
7915
  ctx.drawImage(mc.canvas, -mc.originX, -mc.originY);
7090
7916
  } else if (bmp !== null) {
7917
+ let src = bmp;
7091
7918
  const ti = this.layerTexImage.get(layer.id);
7092
7919
  let sw = ti !== void 0 ? ti[0] : bmp.width;
7093
7920
  let sh = ti !== void 0 ? ti[1] : bmp.height;
@@ -7135,7 +7962,7 @@ void main() {
7135
7962
  canvas: frameBmp
7136
7963
  });
7137
7964
  }
7138
- bmp = frameBmp;
7965
+ src = frameBmp;
7139
7966
  sw = rw;
7140
7967
  sh = rh;
7141
7968
  }
@@ -7151,9 +7978,9 @@ void main() {
7151
7978
  let eff = null;
7152
7979
  if (this.wwGL !== null || WaterwavesGL.available) {
7153
7980
  if (this.wwGL === null) this.wwGL = new WaterwavesGL();
7154
- eff = this.wwGL.render(bmp, sw, sh, maskInfo !== void 0 ? maskInfo.bmp : null, maskInfo !== void 0 ? maskInfo.useA : false, wws, this.animTime, String(layer.id));
7981
+ eff = this.wwGL.render(src, sw, sh, maskInfo !== void 0 ? maskInfo.bmp : null, maskInfo !== void 0 ? maskInfo.useA : false, wws, this.animTime, String(layer.id));
7155
7982
  }
7156
- if (eff === null) eff = applyWaterwaves(bmp, sw, sh, wws, this.animTime, maskInfo !== void 0 ? maskInfo.bmp : null);
7983
+ if (eff === null) eff = applyWaterwaves(src, sw, sh, wws, this.animTime, maskInfo !== void 0 ? maskInfo.bmp : null);
7157
7984
  ctx.drawImage(eff, 0, 0, sw, sh, -dw / 2, -dh / 2, dw, dh);
7158
7985
  } else if (nitros.length > 0) {
7159
7986
  const nt = this.nitroTex.get(layer.id);
@@ -7170,9 +7997,9 @@ void main() {
7170
7997
  smoothness: e.smoothness,
7171
7998
  useMask: e.mask !== null && e.mask !== ""
7172
7999
  }));
7173
- eff = this.nitroGL.render(bmp, sw, sh, nt.noise, nt.masks, params, this.animTime, String(layer.id));
8000
+ eff = this.nitroGL.render(src, sw, sh, nt.noise, nt.masks, params, this.animTime, String(layer.id));
7174
8001
  }
7175
- if (eff === null) ctx.drawImage(bmp, 0, 0, sw, sh, -dw / 2, -dh / 2, dw, dh);
8002
+ if (eff === null) ctx.drawImage(src, 0, 0, sw, sh, -dw / 2, -dh / 2, dw, dh);
7176
8003
  else ctx.drawImage(eff, 0, 0, sw, sh, -dw / 2, -dh / 2, dw, dh);
7177
8004
  } else if (shk !== void 0 && shk.type === "shake") {
7178
8005
  const maskInfo2 = this.effectMasks.get(layer.id);
@@ -7181,8 +8008,8 @@ void main() {
7181
8008
  const amp = shk.strength * shk.strength * effScale;
7182
8009
  const dx = offset * amp * fd[0] * dw;
7183
8010
  const dy = offset * amp * fd[1] * dh;
7184
- ctx.drawImage(bmp, 0, 0, sw, sh, -dw / 2 + dx, -dh / 2 + dy, dw, dh);
7185
- } else ctx.drawImage(bmp, 0, 0, sw, sh, -dw / 2, -dh / 2, dw, dh);
8011
+ ctx.drawImage(src, 0, 0, sw, sh, -dw / 2 + dx, -dh / 2 + dy, dw, dh);
8012
+ } else ctx.drawImage(src, 0, 0, sw, sh, -dw / 2, -dh / 2, dw, dh);
7186
8013
  } else {
7187
8014
  ctx.fillStyle = "rgba(120, 170, 255, 0.5)";
7188
8015
  ctx.beginPath();
@@ -7260,7 +8087,8 @@ void main() {
7260
8087
  "focus",
7261
8088
  "renderMode",
7262
8089
  "gazeEnabled",
7263
- "gazeSnapText"
8090
+ "gazeSnapText",
8091
+ "floater"
7264
8092
  ];
7265
8093
  const asNumber = (v, fallback, min, max) => typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : fallback;
7266
8094
  const asBoolean = (v, fallback) => typeof v === "boolean" ? v : fallback;
@@ -7280,6 +8108,7 @@ void main() {
7280
8108
  renderMode: mode === "eco" || mode === "perf" || mode === "enhanced" ? mode : d.renderMode,
7281
8109
  gazeEnabled: focus && asBoolean(o.gazeEnabled, d.gazeEnabled),
7282
8110
  gazeSnapText: asBoolean(o.gazeSnapText, d.gazeSnapText),
8111
+ floater: asBoolean(o.floater, d.floater),
7283
8112
  taskActive: d.taskActive,
7284
8113
  approvalPending: d.approvalPending,
7285
8114
  immersive: d.immersive
@@ -7298,8 +8127,12 @@ void main() {
7298
8127
  /**
7299
8128
  * 生成「写即存」的设置对象:初始值来自存档,之后每次改动自动落盘。
7300
8129
  * 滑块拖动会高频触发写入,故合并成 250ms 的尾随写;页面隐藏 / 关闭前强制补一次,避免丢最后一次改动。
8130
+ *
8131
+ * onRemoteChange:跨页签同步 —— 另一个页签改了设置(storage 事件只在其他页签触发)时,
8132
+ * 把新值就地写进本页面 target(绕开 Proxy 避免回写震荡)并回调,让面板镜像与悬浮球上报跟上
8133
+ * (否则旧页签内存里 floater=false 会一直上报错误开关值,把别人挂起的球误拆——实测过的坑)。
7301
8134
  */
7302
- function createPersistentSettings(defaults) {
8135
+ function createPersistentSettings(defaults, onRemoteChange) {
7303
8136
  const target = readStoredSettings(defaults);
7304
8137
  let timer = null;
7305
8138
  const flush = () => {
@@ -7321,6 +8154,22 @@ void main() {
7321
8154
  document.addEventListener("visibilitychange", () => {
7322
8155
  if (document.visibilityState === "hidden") flush();
7323
8156
  });
8157
+ if (onRemoteChange !== void 0) window.addEventListener("storage", (e) => {
8158
+ if (e.key !== "we-sync.settings") return;
8159
+ let changed = false;
8160
+ try {
8161
+ const next = readStoredSettings(defaults);
8162
+ const rec = target;
8163
+ for (const key of PERSISTED_KEYS) {
8164
+ const k = key;
8165
+ if (rec[k] !== next[k]) {
8166
+ rec[k] = next[k];
8167
+ changed = true;
8168
+ }
8169
+ }
8170
+ } catch {}
8171
+ if (changed) onRemoteChange();
8172
+ });
7324
8173
  return new Proxy(target, { set(obj, prop, value) {
7325
8174
  const key = prop;
7326
8175
  const rec = obj;
@@ -10239,6 +11088,8 @@ void main(){
10239
11088
  weekday
10240
11089
  };
10241
11090
  }
11091
+ /** 1×1 RGBA、全透明、filter 0 的合法 PNG 字节。 */
11092
+ const TINY_PNG = Uint8Array.from(atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII="), (c) => c.charCodeAt(0));
10242
11093
  //#endregion
10243
11094
  //#region src/client/dwp-stage.ts
10244
11095
  /**
@@ -10247,6 +11098,16 @@ void main(){
10247
11098
  * 浏览器专用(依赖 DOM/canvas/createImageBitmap),不进 Node 测试网;
10248
11099
  * 逻辑与 demo(dwp-runtime-web/demo)同源,复用同一 mount() → 像素一致。
10249
11100
  */
11101
+ /**
11102
+ * 低档位用 1×1 全透明 PNG 顶替高档资源(字节见 tiny-png.ts,带回归测试)。
11103
+ * 为什么是"顶替"而不是"不传":@dwp/web 的 loadAssets 对缺失资源直接抛错(整次挂载失败),
11104
+ * 所以必须给一个合法但极小的位图;这些图层在低档位 alpha 恒 0,画面上完全看不出来。
11105
+ */
11106
+ /** 读取 scene 顶层的插件扩展字段 dsh.hdAssets(未知键 core 会忽略,这里防御式读取)。 */
11107
+ function hdAssetsOf(scene) {
11108
+ const list = scene.dsh?.hdAssets;
11109
+ return Array.isArray(list) ? list.filter((p) => typeof p === "string") : [];
11110
+ }
10250
11111
  /** 拉取并挂载一个已装 DWP 到 canvas,返回可播放/截图/销毁的 Handle。 */
10251
11112
  async function mountDwp(canvas, id, opts = {}) {
10252
11113
  const fetchFn = opts.fetchFn ?? ((u, i) => fetch(u, i));
@@ -10259,9 +11120,15 @@ void main(){
10259
11120
  const mres = await fetchFn(`${base}/manifest?id=${encodeURIComponent(id)}`, { cache: "no-store" });
10260
11121
  if (mres.ok) manifest = await mres.json();
10261
11122
  } catch {}
11123
+ const stub = new Set(opts.quality === "hd" ? [] : hdAssetsOf(scene));
11124
+ const vq = manifest?.version !== void 0 && manifest.version !== "" ? "&v=" + encodeURIComponent(manifest.version) : "";
10262
11125
  const files = /* @__PURE__ */ new Map();
10263
11126
  for (const ref of collectAssetRefs(scene)) {
10264
- const r = await fetchFn(`${base}/file?id=${encodeURIComponent(id)}&name=${encodeURIComponent(ref.path)}`);
11127
+ if (stub.has(ref.path)) {
11128
+ files.set(ref.path, new Blob([TINY_PNG], { type: "image/png" }));
11129
+ continue;
11130
+ }
11131
+ const r = await fetchFn(`${base}/file?id=${encodeURIComponent(id)}&name=${encodeURIComponent(ref.path)}${vq}`);
10265
11132
  if (r.ok) files.set(ref.path, await r.blob());
10266
11133
  }
10267
11134
  return mount(canvas, {
@@ -10286,12 +11153,36 @@ void main(){
10286
11153
  handle = null;
10287
11154
  mountingId = "";
10288
11155
  mountedId = "";
10289
- /** 挂载完成前到达的实时变量(workspace-pulse 喂食等),挂载成功后一次性应用。 */
10290
- pendingVars = null;
11156
+ /**
11157
+ * 最新实时变量(按键累积)。不同来源互不覆盖(时钟 vs 工作区脉搏),
11158
+ * 每次挂载成功后整表重放一次 —— 于是"切到另一张壁纸""切档位重新挂载"都能立刻拿到当前
11159
+ * 时间/档位,而不是干等下一次变量变化(夜间切入日夜壁纸却一直显示白天就是这么来的)。
11160
+ */
11161
+ liveVars = {};
11162
+ /**
11163
+ * 挂载序号:每次 mount/unmount 自增,异步结果只有在序号仍是最新时才允许接管状态。
11164
+ * 只比 id 是不够的 —— 同一张壁纸切画质时新旧请求的 id 相同,旧(低清)结果可能后到达并覆盖新的。
11165
+ */
11166
+ seq = 0;
11167
+ /** 纹理档位:'hd' = 允许拉场景声明的高档纹理,'sd' = 高档资源用占位图顶替(见 dwp-stage.ts) */
11168
+ quality = "sd";
10291
11169
  /** 当前正在挂载或已挂载的 DWP id('' = 无)。 */
10292
11170
  currentId() {
10293
11171
  return this.mountedId !== "" ? this.mountedId : this.mountingId;
10294
11172
  }
11173
+ /**
11174
+ * 切换纹理档位(渲染模式「增强/完整」→ hd,「预览/捕获」→ sd)。
11175
+ * 档位变了必须**重新挂载**:资源集合在 mount 时就定好了(低档位根本没拉 8K 纹理),
11176
+ * 光改变量换不出另一档的图。已挂载时这里先卸载再按新档位挂回去。
11177
+ */
11178
+ setQuality(q) {
11179
+ if (q === this.quality) return;
11180
+ this.quality = q;
11181
+ const id = this.currentId();
11182
+ if (id === "") return;
11183
+ this.unmount();
11184
+ this.mount(id);
11185
+ }
10295
11186
  /** 当前 DOM 中的舞台 canvas(mount 内部 GL→Canvas2D 降级会换新元素,按 data-dwp-stage 定位)。 */
10296
11187
  liveCanvas() {
10297
11188
  return document.querySelector("canvas[data-dwp-stage=\"1\"]");
@@ -10317,29 +11208,30 @@ void main(){
10317
11208
  if (this.canvas.parentNode === null) document.body.appendChild(this.canvas);
10318
11209
  return this.canvas;
10319
11210
  }
10320
- /** 挂载指定 DWP 为背景。同 id 幂等;换 id 先销毁旧的。异步(拉 scene+资源)。 */
10321
- async mount(id) {
10322
- if (this.mountingId === id || this.mountedId === id) return;
11211
+ /** 挂载指定 DWP 为背景。同 id 幂等(除非 force);换 id / 换档位先销毁旧的。异步(拉 scene+资源)。 */
11212
+ async mount(id, opts) {
11213
+ if (opts?.force !== true && (this.mountingId === id || this.mountedId === id)) return;
10323
11214
  this.disposeHandle();
11215
+ const mySeq = ++this.seq;
10324
11216
  this.mountingId = id;
10325
11217
  const canvas = this.ensureCanvas();
10326
11218
  try {
10327
- const handle = await mountDwp(canvas, id, { onDegrade: (d) => {
10328
- if (d.length) console.warn("[dwp] 降级/告警:", d.join(", "));
10329
- } });
10330
- if (this.mountingId !== id) {
11219
+ const handle = await mountDwp(canvas, id, {
11220
+ quality: this.quality,
11221
+ onDegrade: (d) => {
11222
+ if (d.length) console.warn("[dwp] 降级/告警:", d.join(", "));
11223
+ }
11224
+ });
11225
+ if (mySeq !== this.seq) {
10331
11226
  handle.dispose();
10332
11227
  return;
10333
11228
  }
10334
11229
  this.handle = handle;
10335
11230
  this.mountedId = id;
10336
11231
  this.mountingId = "";
10337
- if (this.pendingVars !== null && Object.keys(this.pendingVars).length > 0) {
10338
- handle.setParams(this.pendingVars);
10339
- this.pendingVars = null;
10340
- }
11232
+ if (Object.keys(this.liveVars).length > 0) handle.setParams(this.liveVars);
10341
11233
  } catch (e) {
10342
- if (this.mountingId === id) this.mountingId = "";
11234
+ if (mySeq === this.seq) this.mountingId = "";
10343
11235
  throw e;
10344
11236
  }
10345
11237
  }
@@ -10350,14 +11242,22 @@ void main(){
10350
11242
  c.style.filter = blurPx > 0 ? "blur(" + blurPx + "px)" : "none";
10351
11243
  c.style.transform = "scale(" + scale.toFixed(3) + ")";
10352
11244
  }
10353
- /** 实时变量喂食(workspace-pulse 的改动气泡等):有 Handle 立即 setParams,否则缓存到挂载后应用。 */
11245
+ /**
11246
+ * 实时变量喂食(时钟、工作区脉搏等)。
11247
+ * 挂载中/未挂载时先累加到 liveVars(**按键合并**,不同来源互不覆盖),
11248
+ * 已挂载则把本次的键立即推给运行时(Handle.setParams 本身也是逐键覆写)。
11249
+ */
10354
11250
  setLiveVars(map) {
10355
- if (this.handle === null) {
10356
- this.pendingVars = Object.keys(map).length > 0 ? map : null;
10357
- return;
10358
- }
11251
+ this.liveVars = {
11252
+ ...this.liveVars,
11253
+ ...map
11254
+ };
11255
+ if (this.handle === null) return;
10359
11256
  this.handle.setParams(map);
10360
- if (Object.keys(map).length === 0) this.pendingVars = null;
11257
+ }
11258
+ /** 当前累积的实时变量(测试/诊断用)。 */
11259
+ currentVars() {
11260
+ return { ...this.liveVars };
10361
11261
  }
10362
11262
  disposeHandle() {
10363
11263
  if (this.handle !== null) {
@@ -10367,8 +11267,9 @@ void main(){
10367
11267
  this.mountedId = "";
10368
11268
  this.mountingId = "";
10369
11269
  }
10370
- /** 卸载:停渲染 + 移除画布。 */
11270
+ /** 卸载:停渲染 + 移除画布。序号自增 → 任何在途挂载的结果都会被丢弃。 */
10371
11271
  unmount() {
11272
+ this.seq++;
10372
11273
  this.disposeHandle();
10373
11274
  const c = this.liveCanvas() ?? this.canvas;
10374
11275
  if (c !== null) c.remove();
@@ -10408,6 +11309,126 @@ void main(){
10408
11309
  }
10409
11310
  return vars;
10410
11311
  }
11312
+ const DAY_MINUTES = 1440;
11313
+ /** 一天中的分钟数(0..1439.99…,带秒的小数)→ 夜间透明度 0..1。 */
11314
+ function nightAlpha(minutesOfDay) {
11315
+ const m = (minutesOfDay % DAY_MINUTES + DAY_MINUTES) % DAY_MINUTES;
11316
+ const start = 1080;
11317
+ const nightLen = 720 % DAY_MINUTES;
11318
+ const rel = (m - start + DAY_MINUTES) % DAY_MINUTES;
11319
+ const clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
11320
+ if (rel <= 10) return clamp01(rel / 10);
11321
+ if (rel >= 710 && rel <= nightLen) return clamp01((nightLen - rel) / 10);
11322
+ return rel < nightLen ? 1 : 0;
11323
+ }
11324
+ /** 当前时间(+ 纹理档位)→ 变量表(值域收敛到 4 位小数,避免每次轮询都产生"变化")。 */
11325
+ function clockVars(now, opts = {}) {
11326
+ const hd = opts.hd === true;
11327
+ const minutes = now.getHours() * 60 + now.getMinutes() + now.getSeconds() / 60;
11328
+ const alpha = Number(nightAlpha(minutes).toFixed(4));
11329
+ return {
11330
+ hour: now.getHours(),
11331
+ night_alpha: alpha,
11332
+ night_on: alpha >= .9999 ? 1 : 0,
11333
+ day_on: alpha <= 1e-4 ? 1 : 0,
11334
+ hd_on: hd ? 1 : 0,
11335
+ night_sd: hd ? 0 : alpha,
11336
+ night_hd: hd ? alpha : 0
11337
+ };
11338
+ }
11339
+ /** 去重签名:值没变就不喂(避免无谓重绘)。 */
11340
+ function clockSig(vars) {
11341
+ return Object.keys(vars).sort().map((k) => k + "=" + String(vars[k])).join(",");
11342
+ }
11343
+ //#endregion
11344
+ //#region src/client/floater-report.ts
11345
+ const FLOATER_URL = "/we-sync/floater";
11346
+ const FLOATER_HEARTBEAT_MS = 5e3;
11347
+ function startFloaterReporter(env, isEnabled) {
11348
+ let lastColor = "";
11349
+ let blurred = false;
11350
+ let blurTimer = 0;
11351
+ let disposed = false;
11352
+ function current() {
11353
+ return env.visibilityState() === "hidden" || blurred ? "hidden" : "visible";
11354
+ }
11355
+ let lastSent = current();
11356
+ const send = (state, keepalive) => {
11357
+ try {
11358
+ env.send(FLOATER_URL, JSON.stringify({
11359
+ pageId: env.pageId(),
11360
+ enabled: isEnabled(),
11361
+ state,
11362
+ title: env.title(),
11363
+ color: lastColor !== "" ? lastColor : void 0
11364
+ }), keepalive);
11365
+ } catch {}
11366
+ };
11367
+ /** 只在状态相对上次已发送值变化时发(blur/focus 抖动、重复事件不刷请求)。 */
11368
+ const emit = (keepalive) => {
11369
+ if (disposed) return;
11370
+ const s = current();
11371
+ if (s === lastSent) return;
11372
+ lastSent = s;
11373
+ send(s, keepalive);
11374
+ };
11375
+ const cancelBlur = () => {
11376
+ if (blurTimer !== 0) {
11377
+ env.clearTimeout(blurTimer);
11378
+ blurTimer = 0;
11379
+ }
11380
+ };
11381
+ const offs = [
11382
+ env.onVisibilityChange(() => emit(true)),
11383
+ env.onPageHide(() => send("unloading", true)),
11384
+ env.onBlur(() => {
11385
+ if (blurTimer !== 0 || blurred) return;
11386
+ blurTimer = env.setTimeout(() => {
11387
+ blurTimer = 0;
11388
+ blurred = true;
11389
+ emit(true);
11390
+ }, 400);
11391
+ }),
11392
+ env.onFocus(() => {
11393
+ cancelBlur();
11394
+ blurred = false;
11395
+ emit(true);
11396
+ })
11397
+ ];
11398
+ const heartbeat = env.setInterval(() => {
11399
+ if (!disposed && isEnabled() && current() === "visible") {
11400
+ lastSent = "visible";
11401
+ send("visible", true);
11402
+ }
11403
+ }, FLOATER_HEARTBEAT_MS);
11404
+ send(lastSent, false);
11405
+ return {
11406
+ syncNow() {
11407
+ const s = current();
11408
+ lastSent = s;
11409
+ send(s, false);
11410
+ },
11411
+ pingColor(color) {
11412
+ if (color === lastColor || !isEnabled()) return;
11413
+ lastColor = color;
11414
+ const s = current();
11415
+ lastSent = s;
11416
+ send(s, false);
11417
+ },
11418
+ get state() {
11419
+ return current();
11420
+ },
11421
+ get lastColor() {
11422
+ return lastColor;
11423
+ },
11424
+ dispose() {
11425
+ disposed = true;
11426
+ cancelBlur();
11427
+ for (const off of offs) off();
11428
+ env.clearInterval(heartbeat);
11429
+ }
11430
+ };
11431
+ }
10411
11432
  //#endregion
10412
11433
  //#region src/client/index.ts
10413
11434
  /**
@@ -10419,7 +11440,7 @@ void main(){
10419
11440
  */
10420
11441
  const inject = ["slots", "theme"];
10421
11442
  /** 插件版本:构建期由 tsdown 的 define 从 package.json 注入(未注入的裸跑为 'dev')。 */
10422
- const PLUGIN_VERSION = "26.9.10";
11443
+ const PLUGIN_VERSION = "26.9.13";
10423
11444
  /** 插件 GitHub 仓库主页(版本号角标点击跳转)。 */
10424
11445
  const PLUGIN_REPO_URL = "https://github.com/YRN-playmaker/dsh-wallpaper_share";
10425
11446
  /** 专注模式:任务进行中(本版下调的全局值;鼠标圆内另按 FOCUS_LENS 加浓) */
@@ -10461,6 +11482,7 @@ void main(){
10461
11482
  renderMode: "perf",
10462
11483
  gazeEnabled: false,
10463
11484
  gazeSnapText: true,
11485
+ floater: false,
10464
11486
  immersive: false,
10465
11487
  approvalPending: false,
10466
11488
  dwpMounted: null
@@ -10475,8 +11497,13 @@ void main(){
10475
11497
  /** 用户偏好(同步开关 / 渲染模式 / 显示器锁 / 透明度·模糊·阴影 / 专注·眼动)经 localStorage 持久化:
10476
11498
  * 写即存,刷新或重启 DSH 后自动恢复。实现见 settings.ts —— 包一层 Proxy,所有
10477
11499
  * `store.settings.x = v` 的既有赋值点无需改动即自动落盘;派生态(taskActive /
10478
- * approvalPending)与临时视图态(immersive)不在落盘白名单内。 */
10479
- settings: createPersistentSettings(DEFAULT_SETTINGS),
11500
+ * approvalPending)与临时视图态(immersive)不在落盘白名单内。
11501
+ * 跨页签改动(storage 事件)就地同步并回调:notify 刷面板,syncFloater 让本页面立刻
11502
+ * 上报新的 enabled 值——不然旧页签的内存态会把别人挂起的球误拆。 */
11503
+ settings: createPersistentSettings(DEFAULT_SETTINGS, () => {
11504
+ store.notify();
11505
+ store.actions.syncFloater();
11506
+ }),
10480
11507
  listeners: /* @__PURE__ */ new Set(),
10481
11508
  actions: {
10482
11509
  applyTheme: () => {},
@@ -10484,7 +11511,9 @@ void main(){
10484
11511
  applyImmersive: () => {},
10485
11512
  repoll: () => {},
10486
11513
  mountDwp: async (_id) => false,
10487
- unmountDwp: async () => {}
11514
+ unmountDwp: async () => {},
11515
+ /** 悬浮球开关变更后立即重发 sync(真实现在下方 ctx.effect 里覆盖) */
11516
+ syncFloater: () => {}
10488
11517
  },
10489
11518
  subscribe(fn) {
10490
11519
  store.listeners.add(fn);
@@ -10607,6 +11636,55 @@ void main(){
10607
11636
  running: "#3b82f6",
10608
11637
  idle: "#22c55e"
10609
11638
  };
11639
+ const floaterPageId = () => {
11640
+ try {
11641
+ let id = sessionStorage.getItem("we-sync.floater-page");
11642
+ if (id === null) {
11643
+ id = Math.random().toString(36).slice(2) + Date.now().toString(36);
11644
+ sessionStorage.setItem("we-sync.floater-page", id);
11645
+ }
11646
+ return id;
11647
+ } catch {
11648
+ return "legacy";
11649
+ }
11650
+ };
11651
+ let floaterReporter = null;
11652
+ try {
11653
+ floaterReporter = startFloaterReporter({
11654
+ visibilityState: () => document.visibilityState === "hidden" ? "hidden" : "visible",
11655
+ title: () => document.title,
11656
+ pageId: floaterPageId,
11657
+ onFocus: (fn) => {
11658
+ window.addEventListener("focus", fn);
11659
+ return () => window.removeEventListener("focus", fn);
11660
+ },
11661
+ onBlur: (fn) => {
11662
+ window.addEventListener("blur", fn);
11663
+ return () => window.removeEventListener("blur", fn);
11664
+ },
11665
+ onVisibilityChange: (fn) => {
11666
+ document.addEventListener("visibilitychange", fn);
11667
+ return () => document.removeEventListener("visibilitychange", fn);
11668
+ },
11669
+ onPageHide: (fn) => {
11670
+ window.addEventListener("pagehide", fn);
11671
+ return () => window.removeEventListener("pagehide", fn);
11672
+ },
11673
+ setInterval: (fn, ms) => window.setInterval(fn, ms),
11674
+ clearInterval: (handle) => window.clearInterval(handle),
11675
+ setTimeout: (fn, ms) => window.setTimeout(fn, ms),
11676
+ clearTimeout: (handle) => window.clearTimeout(handle),
11677
+ send: (url, body, keepalive) => {
11678
+ fetch(url, {
11679
+ method: "POST",
11680
+ headers: { "Content-Type": "application/json" },
11681
+ body,
11682
+ keepalive
11683
+ }).catch(() => {});
11684
+ }
11685
+ }, () => store.settings.floater);
11686
+ store.actions.syncFloater = () => floaterReporter?.syncNow();
11687
+ } catch {}
10610
11688
  function syncStatus() {
10611
11689
  const approval = document.querySelector("[data-approval-key]") !== null;
10612
11690
  if (approval !== store.settings.approvalPending) {
@@ -10615,6 +11693,7 @@ void main(){
10615
11693
  }
10616
11694
  const color = approval ? STATUS_COLORS.approval : store.settings.taskActive ? STATUS_COLORS.running : STATUS_COLORS.idle;
10617
11695
  orbBtn.style.borderColor = color;
11696
+ floaterReporter?.pingColor(color.slice(1));
10618
11697
  orbBtn.title = approval ? "等待授权" : store.settings.taskActive ? "任务进行中" : "空闲";
10619
11698
  const sidebarCollapsed = document.querySelector("[data-sidebar-collapsed]") !== null;
10620
11699
  orbBtn.style.opacity = sidebarCollapsed ? "1" : "0";
@@ -10851,6 +11930,10 @@ void main(){
10851
11930
  attributes: true,
10852
11931
  attributeFilter: ["data-sidebar-collapsed"]
10853
11932
  });
11933
+ /** 渲染模式 → DWP 纹理档位:「增强/完整」用高档纹理,其余(预览/捕获)用低档。 */
11934
+ function qualityOf(mode) {
11935
+ return mode === "enhanced" ? "hd" : "sd";
11936
+ }
10854
11937
  function applyBackground() {
10855
11938
  if (store.settings.dwpMounted !== null) {
10856
11939
  const blurPx = store.settings.focus ? 0 : Math.round(effectiveVisuals().blur);
@@ -10859,6 +11942,7 @@ void main(){
10859
11942
  stopSceneLayers();
10860
11943
  setMedia(null);
10861
11944
  styleTag.textContent = "html { background-color: #0d0e12; }body::after { content: \"\"; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; background: linear-gradient(rgba(6,8,12," + shadowAlpha.toFixed(3) + "), rgba(6,8,12," + (shadowAlpha * .85).toFixed(3) + ")); }";
11945
+ dwpBg.setQuality(qualityOf(store.settings.renderMode));
10862
11946
  dwpBg.mount(store.settings.dwpMounted).then(() => dwpBg.applyVisuals(blurPx, scale)).catch((e) => {
10863
11947
  console.error("[dwp] 背景挂载失败:", e);
10864
11948
  });
@@ -10955,6 +12039,19 @@ void main(){
10955
12039
  dwpBg.setLiveVars(map);
10956
12040
  } catch {}
10957
12041
  }
12042
+ let lastClockSig = "";
12043
+ function feedClockVars() {
12044
+ if (store.settings.dwpMounted === null) {
12045
+ lastClockSig = "";
12046
+ return;
12047
+ }
12048
+ const mounted = store.settings.dwpMounted;
12049
+ const vars = clockVars(/* @__PURE__ */ new Date(), { hd: qualityOf(store.settings.renderMode) === "hd" });
12050
+ const sig = mounted + "|" + clockSig(vars);
12051
+ if (sig === lastClockSig) return;
12052
+ lastClockSig = sig;
12053
+ dwpBg.setLiveVars(vars);
12054
+ }
10958
12055
  async function poll() {
10959
12056
  if (polling) return;
10960
12057
  polling = true;
@@ -10977,6 +12074,7 @@ void main(){
10977
12074
  applyBackground();
10978
12075
  }
10979
12076
  await pollWorkspacePulse();
12077
+ feedClockVars();
10980
12078
  } catch {}
10981
12079
  polling = false;
10982
12080
  }
@@ -10993,7 +12091,6 @@ void main(){
10993
12091
  if (store.settings.dwpMounted === null) dwpPrevEnabled = store.settings.enabled;
10994
12092
  store.settings.dwpMounted = id;
10995
12093
  store.settings.enabled = false;
10996
- if (store.settings.renderMode === "perf") store.settings.renderMode = "enhanced";
10997
12094
  store.notify();
10998
12095
  applyBackground();
10999
12096
  return true;
@@ -11010,7 +12107,6 @@ void main(){
11010
12107
  dwpPrevEnabled = true;
11011
12108
  store.settings.dwpMounted = applied.id;
11012
12109
  store.settings.enabled = false;
11013
- if (store.settings.renderMode === "perf") store.settings.renderMode = "enhanced";
11014
12110
  store.notify();
11015
12111
  applyBackground();
11016
12112
  }).catch(() => {});
@@ -11019,6 +12115,9 @@ void main(){
11019
12115
  panelStyleTag.remove();
11020
12116
  immersiveStyleTag.remove();
11021
12117
  orbBtn.remove();
12118
+ floaterReporter?.dispose();
12119
+ floaterReporter = null;
12120
+ store.actions.syncFloater = () => {};
11022
12121
  destroyFocusLens();
11023
12122
  statusObserver.disconnect();
11024
12123
  document.removeEventListener("keydown", onImmersiveKey);