dsh-plugin-wallpaper-engine-media-bridge 1.4.0 → 1.4.2
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/cordis.patch.yml +5 -2
- package/lib/client.media-bridge.js +835 -0
- package/lib/index.js +119 -6
- package/lib/legacy.js +2 -0
- package/lib/occlusion/index.js +292 -0
- package/lib/routes/inventory.js +142 -0
- package/lib/routes/live-frame.js +97 -0
- package/lib/routes/media-bytes.js +90 -0
- package/lib/routes/media-derived.js +250 -0
- package/lib/routes/now-playing.js +2 -2
- package/lib/routes/occlusion.js +78 -0
- package/lib/routes/props.js +81 -0
- package/lib/routes/scene-frame.js +373 -0
- package/lib/routes/scene-media.js +174 -0
- package/lib/routes/scene-serve.js +195 -0
- package/lib/routes/slides.js +68 -0
- package/lib/routes/upload.js +355 -0
- package/lib/routes/we-assets.js +172 -0
- package/lib/supervisor.js +1 -1
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +8 -2
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-plugin-wallpaper-engine-media-bridge",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
const host = require("dsh-plugin-wallpaper-engine");
|
|
8
|
+
// ── 内联模块:packages/media-bridge/src/wallpaper-current-section.js —— 「壁纸库」页签第一节「当前壁纸」的渲染器(ADR-0013/ADR-0014 子包出户);入口的 registerPanelSection 把它的 render 登记进面板节注册表(library,order 10)(构建期注入;勿手改本段)──
|
|
9
|
+
/**
|
|
10
|
+
* 「壁纸库」页签第一节「当前壁纸」的渲染器。
|
|
11
|
+
*
|
|
12
|
+
* 归属(ADR-0013 D20):随包走的是**渲染器**;当前壁纸的 selection、播放动作体、
|
|
13
|
+
* 失败原因计算仍在主包(src/client.js / src/live-layer.js)⇒ 本文件**不持键、不写 selection、
|
|
14
|
+
* 不调 emit**,只读 ctx;跨包通道只有主 bundle 的显式 host 键集:
|
|
15
|
+
* · VinylRecord / vinylSpinVisible(src/client.js 工厂作用域)· liveFailReasonOf(src/live-layer.js)
|
|
16
|
+
* · 四节共享的 React / weT
|
|
17
|
+
* 内联形状见 packages/media-bridge/client.build.json;入口的 registerPanelSection 把 render 登记进
|
|
18
|
+
* 面板节注册表(library,order 10)。与同族三节共享同一契约:只读 ctx、不写 selection、不调 emit
|
|
19
|
+
* (判据扫全族并集,见 test/verify-scene-live.mjs / test/verify-client.mjs)。
|
|
20
|
+
*/
|
|
21
|
+
function renderWallpaperCurrentSection(ctx) {
|
|
22
|
+
const { current, isLiveScene, onClear, onOpenPicker, onRefresh, onToggleAudio, onTogglePlay, onTogglePropsPanel, playbackLive, propsPanelOpen, renderUserPropsPanel, sel, setPickerOpener } = ctx;
|
|
23
|
+
return React.createElement(React.Fragment, null,
|
|
24
|
+
// ── 当前壁纸: vinyl record beside the selection, in both card styles. ──
|
|
25
|
+
React.createElement("div", { className: "we-picker__section" },
|
|
26
|
+
React.createElement("div", { className: "we-picker__section-head" },
|
|
27
|
+
React.createElement("span", { className: "we-picker__section-label" }, weT("当前壁纸")),
|
|
28
|
+
),
|
|
29
|
+
React.createElement("div", { className: "we-picker__current" },
|
|
30
|
+
React.createElement(VinylRecord, {
|
|
31
|
+
cover: current && current.preview, title: current ? current.title : "",
|
|
32
|
+
playing: playbackLive && Boolean(sel.url) && vinylSpinVisible(),
|
|
33
|
+
}),
|
|
34
|
+
React.createElement("div", { className: "we-picker__current-info" },
|
|
35
|
+
React.createElement("div", { className: "we-picker__current-title", title: current ? current.title : "" },
|
|
36
|
+
sel.id && current ? current.title : weT("未选择壁纸"),
|
|
37
|
+
// 类型 + 播放态:宽卡片里另起一行;抽屉(窄容器)里由 CSS 改成
|
|
38
|
+
// 「名称(类型 · 播放中)」同一行,整行超出省略(见 .we-repo-panel 规则)。
|
|
39
|
+
React.createElement("span", { className: "we-picker__current-meta" },
|
|
40
|
+
current
|
|
41
|
+
? weT("{type}{status}", {
|
|
42
|
+
type: { video: weT("视频壁纸"), web: weT("网页壁纸"), image: weT("图片壁纸"), scene: weT(isLiveScene ? "场景壁纸(实时渲染)" : "场景壁纸(静态帧)") }[current.type] || weT("壁纸"),
|
|
43
|
+
status: weT(playbackLive ? " · 播放中" : " · 已暂停"),
|
|
44
|
+
})
|
|
45
|
+
: weT("尚未选择壁纸"))),
|
|
46
|
+
// 失败/被过滤的原因说明包一层:抽屉里该层用 display:contents 展开成
|
|
47
|
+
// grid 项(标题已独走第一行),靠这个包裹层保证「一行一项」。
|
|
48
|
+
React.createElement("div", { className: "we-picker__current-sub" },
|
|
49
|
+
// 播放失败原因(#84): 浏览器解不了的编码 / 解码失败等 —— 给出可读原因,
|
|
50
|
+
// 配合下面的「播放」按钮重试(不是静默空白)。
|
|
51
|
+
sel.videoError && React.createElement("div", { className: "we-picker__current-error" }, sel.videoError),
|
|
52
|
+
// 选择被过滤条件排除(#84): 明确指出是哪一项过滤挡住了、怎么恢复
|
|
53
|
+
sel.blockedNote && React.createElement("div", { className: "we-picker__current-error" }, sel.blockedNote),
|
|
54
|
+
// 实时渲染失败原因(自动回退到旧链时显示):让「为什么黑」可见 ——
|
|
55
|
+
// 用户反馈时能直接说明,也提示了重试入口(重开「实时渲染」开关)。
|
|
56
|
+
liveFailReasonOf(sel) && React.createElement("div", { className: "we-picker__current-error" },
|
|
57
|
+
weT("实时渲染失败({reason}),已自动回退;重新打开「实时渲染」开关可重试", { reason: weT(liveFailReasonOf(sel)) }))
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
// 主操作区:选择壁纸(下钻库视图)。壁纸属性入口在下面那排播放控制里、
|
|
61
|
+
// 排在「暂停」之前(同排同级、同款式)。
|
|
62
|
+
React.createElement("div", { className: "we-picker__current-actions" },
|
|
63
|
+
React.createElement("button", {
|
|
64
|
+
className: "we-picker__btn we-picker__btn--primary", type: "button",
|
|
65
|
+
ref: setPickerOpener,
|
|
66
|
+
onClick: onOpenPicker,
|
|
67
|
+
}, weT("选择壁纸")),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
// 属性面板紧贴卡片下方(同一节里),抽屉/弹窗两种形态都可见。
|
|
71
|
+
renderUserPropsPanel(),
|
|
72
|
+
// Playback controls (wallpaper-independent; the thumbnail grid lives in
|
|
73
|
+
// the modal above, so these stay within reach).
|
|
74
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
75
|
+
// 壁纸属性(仅场景/网页壁纸 + 有 propsUrl):与播放控制同排、同款式
|
|
76
|
+
//(普通 .we-picker__btn;面板开着时 is-on 高亮)——入口与播放控制同级更顺手。
|
|
77
|
+
(current && (current.type === "scene" || current.type === "web") && sel.propsUrl)
|
|
78
|
+
&& React.createElement("button", {
|
|
79
|
+
className: "we-picker__btn" + (propsPanelOpen ? " is-on" : ""),
|
|
80
|
+
type: "button",
|
|
81
|
+
title: weT("壁纸作者提供的可调属性(改动立即生效)"),
|
|
82
|
+
onClick: onTogglePropsPanel,
|
|
83
|
+
}, weT("壁纸属性")),
|
|
84
|
+
React.createElement("button", {
|
|
85
|
+
className: "we-picker__btn", type: "button",
|
|
86
|
+
onClick: onTogglePlay, disabled: !sel.url,
|
|
87
|
+
// 按钮显示真实状态(#84): 播放失败时回到「播放」,就是用户要的「继续」。
|
|
88
|
+
}, playbackLive ? weT("暂停") : weT("播放", null, "play")),
|
|
89
|
+
// 音乐开关:与「播放」同级的一键切换。只影响音轨,不动播放态 ——
|
|
90
|
+
// 关掉后画面继续播放。仅对含音轨的壁纸类型显示(视频 / 场景内嵌 MP4)。
|
|
91
|
+
//
|
|
92
|
+
// ⚠️ 高亮判据必须与**按钮自己的状态**(也就是下面文案那个判据)**逐字同一个**:
|
|
93
|
+
// 这颗按钮切的是 `videoAudioEnabled`,所以「亮」= 该开关为开。按别的量(如音量)判亮
|
|
94
|
+
// 会让文案与高亮互相矛盾、点击没有任何视觉反馈。
|
|
95
|
+
// 音量是**另一颗**控件,不参与这颗按钮的开关状态。
|
|
96
|
+
(sel.type === "video" || (sel.type === "scene" && (sel.sceneVideo || sel.sceneHasAudio)))
|
|
97
|
+
&& React.createElement("button", {
|
|
98
|
+
className: "we-picker__btn" + (sel.videoAudioEnabled === false ? "" : " is-on"),
|
|
99
|
+
type: "button",
|
|
100
|
+
onClick: onToggleAudio,
|
|
101
|
+
disabled: !sel.url,
|
|
102
|
+
title: sel.videoAudioEnabled === false
|
|
103
|
+
? weT("开启壁纸音轨(音量为 0 时自动设为 50%)")
|
|
104
|
+
: weT("关闭壁纸音轨(画面继续播放)"),
|
|
105
|
+
}, sel.videoAudioEnabled === false ? weT("🔇 音乐关") : weT("🔊 音乐开")),
|
|
106
|
+
React.createElement("button", {
|
|
107
|
+
className: "we-picker__btn", type: "button",
|
|
108
|
+
onClick: onClear, disabled: !sel.id,
|
|
109
|
+
title: weT("清除当前壁纸(停止播放,回到无壁纸状态)"),
|
|
110
|
+
}, weT("清除")),
|
|
111
|
+
React.createElement("button", {
|
|
112
|
+
className: "we-picker__btn", type: "button",
|
|
113
|
+
onClick: onRefresh, disabled: sel.loading,
|
|
114
|
+
}, sel.loading ? weT("刷新中…") : weT("刷新")),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── 内联模块:packages/media-bridge/src/wallpaper-transition-section.js —— 「壁纸库」页签第二节「切换过场」的渲染器(ADR-0013/ADR-0014 子包出户);入口的 registerPanelSection 把它的 render 登记进面板节注册表(library,order 20)(构建期注入;勿手改本段)──
|
|
121
|
+
/**
|
|
122
|
+
* wallpaper-transition-section.js — 「壁纸库」页签第二节「切换过场」的渲染器。
|
|
123
|
+
*
|
|
124
|
+
* 归属 / 构建期形状 / 消费侧契约见本包 `src/client.js` 文件头(同一份 P-D 口径)。
|
|
125
|
+
* 本文件是 `packages/media-bridge/client.build.json` 的 inline 模块:与入口落在**同一个工厂
|
|
126
|
+
* 作用域**,不收 import / require;自由标识符(React / weT / ctlText / switchTransitionOf /
|
|
127
|
+
* SWITCH_TRANSITIONS / SWITCH_DIRS / SWITCH_DIR_LABELS / SWITCH_SPEEDS)由入口从 host 显式键集解构。
|
|
128
|
+
*
|
|
129
|
+
* ctx 键(onSwitchTransition / onSwitchTransitionDir / onSwitchTransitionSpeed / sel)
|
|
130
|
+
* 由主包 renderWallpaperTab 在渲染处构造 ⇒ 本渲染器只读 ctx。
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
function renderWallpaperTransitionSection(ctx) {
|
|
134
|
+
const { onSwitchTransition, onSwitchTransitionDir, onSwitchTransitionSpeed, sel } = ctx;
|
|
135
|
+
const switchTr = switchTransitionOf(sel);
|
|
136
|
+
return React.createElement(React.Fragment, null,
|
|
137
|
+
// ── 切换过场(手动点选与自动轮播共用):类型 / 方向 / 速度 ──
|
|
138
|
+
// 默认「硬切」(零成本、零风险);换默认改 DEFAULTS 一处即可。
|
|
139
|
+
// 每种过场只动 transform / opacity / clip-path(见 switchFrames)。
|
|
140
|
+
React.createElement("div", { className: "we-picker__section" },
|
|
141
|
+
React.createElement("div", { className: "we-picker__section-head" },
|
|
142
|
+
React.createElement("span", { className: "we-picker__section-label" }, weT("切换过场")),
|
|
143
|
+
),
|
|
144
|
+
// 过场动画:**下拉菜单**(不用水平平铺)—— 过场会持续增加,平铺一排按钮
|
|
145
|
+
// 迟早挤成两行、还会把「方向 / 时长」挤下去;下拉天然可扩展。
|
|
146
|
+
React.createElement("div", { className: "we-picker__ctl" },
|
|
147
|
+
ctlText(weT("过场动画"), weT("换壁纸时的转场")),
|
|
148
|
+
React.createElement("select", {
|
|
149
|
+
className: "we-picker__select",
|
|
150
|
+
value: sel.switchTransition,
|
|
151
|
+
onChange: (e) => onSwitchTransition(e.target.value),
|
|
152
|
+
"aria-label": weT("过场动画"),
|
|
153
|
+
},
|
|
154
|
+
...SWITCH_TRANSITIONS.map((t) =>
|
|
155
|
+
React.createElement("option", { key: t.id, value: t.id }, t.label)),
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
// 方向:只有方向型过场(推移 / 擦除 / 条带)听它;条带用它决定竖条 / 横条。
|
|
159
|
+
switchTr.directional && React.createElement("div", { className: "we-picker__ctl we-picker__ctl--wrap" },
|
|
160
|
+
ctlText(weT("方向"), weT("新画面从哪边进")),
|
|
161
|
+
React.createElement("div", { className: "we-picker__seg" },
|
|
162
|
+
SWITCH_DIRS.map((d) =>
|
|
163
|
+
React.createElement("button", {
|
|
164
|
+
key: d,
|
|
165
|
+
className: "we-picker__btn we-picker__rate" + (sel.switchTransitionDir === d ? " we-picker__rate--active" : ""),
|
|
166
|
+
type: "button",
|
|
167
|
+
onClick: () => onSwitchTransitionDir(d),
|
|
168
|
+
"aria-pressed": sel.switchTransitionDir === d ? "true" : "false",
|
|
169
|
+
"aria-label": weT("过场方向 {dir}", { dir: weT(SWITCH_DIR_LABELS[d]) }),
|
|
170
|
+
}, SWITCH_DIR_LABELS[d]),
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
// 时长档:只影响速度乘子,基准时长写在各过场里(见 SWITCH_TRANSITIONS)。
|
|
175
|
+
switchTr.ms > 0 && React.createElement("div", { className: "we-picker__ctl we-picker__ctl--wrap" },
|
|
176
|
+
ctlText(weT("时长"), weT("当前约 {ms} ms", { ms: switchTr.ms })),
|
|
177
|
+
React.createElement("div", { className: "we-picker__seg" },
|
|
178
|
+
SWITCH_SPEEDS.map((s) =>
|
|
179
|
+
React.createElement("button", {
|
|
180
|
+
key: s.id,
|
|
181
|
+
className: "we-picker__btn we-picker__rate" + (sel.switchTransitionSpeed === s.id ? " we-picker__rate--active" : ""),
|
|
182
|
+
type: "button",
|
|
183
|
+
onClick: () => onSwitchTransitionSpeed(s.id),
|
|
184
|
+
"aria-pressed": sel.switchTransitionSpeed === s.id ? "true" : "false",
|
|
185
|
+
"aria-label": weT("过场速度 {speed}", { speed: weT(s.label) }),
|
|
186
|
+
}, s.label),
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── 内联模块:packages/media-bridge/src/wallpaper-rotation-section.js —— 「壁纸库」页签第三节「自动轮播」的渲染器(ADR-0013/ADR-0014 子包出户);入口的 registerPanelSection 把它的 render 登记进面板节注册表(library,order 30)(构建期注入;勿手改本段)──
|
|
195
|
+
/**
|
|
196
|
+
* wallpaper-rotation-section.js — 「壁纸库」页签第三节「自动轮播」的渲染器。
|
|
197
|
+
*
|
|
198
|
+
* 归属 / 构建期形状 / 消费侧契约见本包 `src/client.js` 文件头(同一份 P-D 口径)。
|
|
199
|
+
* 本文件是 `packages/media-bridge/client.build.json` 的 inline 模块:与入口同工厂作用域,
|
|
200
|
+
* 不收 import / require;自由标识符(React / weT / ctlText / Toggle / renderConfirmRow /
|
|
201
|
+
* groupWallpapers / startCreateGroup / startEditGroup / saveEditingGroup / cancelEditGroup /
|
|
202
|
+
* importPlaylistIntoDraft / rotationIntervalDefault)由入口从 host 显式键集解构。
|
|
203
|
+
*
|
|
204
|
+
* 默认轮播间隔经**窄键** `rotationIntervalDefault` 从 host 取(同一格默认值的显式绑定;
|
|
205
|
+
* 真源仍是主包 `DEFAULTS.rotationInterval`,同源判据见 `test/verify-client.mjs`)。
|
|
206
|
+
*
|
|
207
|
+
* ctx 键(INTERVALS / armedConfirm / editing / group / groups / 十一个 on* 回调 /
|
|
208
|
+
* playableCount / sel)照旧由主包 renderWallpaperTab 构造 ⇒ 本函数与搬前逐字同行为
|
|
209
|
+
*(唯一新增:`onPictureSlideSeconds` = 图内过场周期,「图片轮播」那一行用)。
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
function renderWallpaperRotationSection(ctx) {
|
|
213
|
+
const { INTERVALS, armedConfirm, editing, group, groups, onArmDeleteGroup, onDeleteGroup, onDisarmConfirm, onEditInterval, onEditName, onEditOrder, onEditTrigger, onGroupChange, onGroupInterval, onGroupTrigger, onOpenPickerDraft, onPictureSlideSeconds, onToggleRotation, playableCount, sel } = ctx;
|
|
214
|
+
// 图内过场的可选项(秒)。与主包 KINDS.pictureSlideSeconds 的 0..3600 上界一致:
|
|
215
|
+
// 这里只是**常用档**;用户在别处写入的秒数若不在档上,补一条同值 option 回显
|
|
216
|
+
//(否则 select 会因找不到匹配值显示空白,看起来像"设置丢了")。
|
|
217
|
+
const PICTURE_SLIDE_INTERVALS = [0, 5, 10, 15, 30, 60, 180, 300];
|
|
218
|
+
const rawPictureSlideSeconds = Math.max(0, Math.min(3600, Math.round(Number(sel.pictureSlideSeconds) || 0)));
|
|
219
|
+
const pictureSlideOptions = PICTURE_SLIDE_INTERVALS.includes(rawPictureSlideSeconds)
|
|
220
|
+
? PICTURE_SLIDE_INTERVALS
|
|
221
|
+
: PICTURE_SLIDE_INTERVALS.concat([rawPictureSlideSeconds]).sort((a, b) => a - b);
|
|
222
|
+
const groupToken = group ? "group:" + group.id : "";
|
|
223
|
+
return React.createElement(React.Fragment, null,
|
|
224
|
+
// ── 自动轮播: user-defined carousel lists, each with its own
|
|
225
|
+
// wallpaper set, interval and order. Persisted as `rotationGroups` (host config.json). ──
|
|
226
|
+
React.createElement("div", { className: "we-picker__section" },
|
|
227
|
+
React.createElement("div", { className: "we-picker__section-head" },
|
|
228
|
+
React.createElement("span", { className: "we-picker__section-label" }, weT("自动轮播")),
|
|
229
|
+
),
|
|
230
|
+
React.createElement("div", { className: "we-picker__row we-picker__playlist-row" },
|
|
231
|
+
React.createElement("select", {
|
|
232
|
+
className: "we-picker__playlist-select",
|
|
233
|
+
value: sel.rotationGroupId,
|
|
234
|
+
onChange: onGroupChange,
|
|
235
|
+
disabled: groups.length === 0,
|
|
236
|
+
"aria-label": weT("轮播列表"),
|
|
237
|
+
},
|
|
238
|
+
React.createElement("option", { value: "" }, groups.length ? weT("— 选择轮播列表 —") : weT("— 暂无轮播列表 —")),
|
|
239
|
+
...groups.map((g) => React.createElement("option", {
|
|
240
|
+
key: g.id, value: g.id,
|
|
241
|
+
}, weT("{name}({count} 可播放 · {interval} 分钟)", { name: g.name, count: groupWallpapers(g).length, interval: g.interval }))),
|
|
242
|
+
),
|
|
243
|
+
React.createElement("button", {
|
|
244
|
+
className: "we-picker__btn", type: "button",
|
|
245
|
+
onClick: startCreateGroup,
|
|
246
|
+
}, weT("新建")),
|
|
247
|
+
React.createElement("button", {
|
|
248
|
+
className: "we-picker__btn", type: "button",
|
|
249
|
+
onClick: () => startEditGroup(sel.rotationGroupId),
|
|
250
|
+
disabled: !sel.rotationGroupId,
|
|
251
|
+
}, weT("编辑")),
|
|
252
|
+
React.createElement("button", {
|
|
253
|
+
className: "we-picker__btn", type: "button",
|
|
254
|
+
// 第一下只置令牌(`onArmDeleteGroup`);落地在下面那行问句的「确认」里。
|
|
255
|
+
// 待确认时**置灰**(不隐藏、不换位置):按钮仍占着那一格,宽度就完全不变,
|
|
256
|
+
// 而且它指路到问句行 —— 留着可点会让"再点一下是不是就删了"变成猜测。
|
|
257
|
+
onClick: onArmDeleteGroup,
|
|
258
|
+
disabled: !sel.rotationGroupId || armedConfirm === groupToken,
|
|
259
|
+
title: armedConfirm === groupToken
|
|
260
|
+
? weT("已经问过你了 —— 在下面那一行选「确认」或「取消」")
|
|
261
|
+
: weT("删除这个轮播列表(会再问一次)"),
|
|
262
|
+
}, weT("删除")),
|
|
263
|
+
),
|
|
264
|
+
renderConfirmRow(armedConfirm, groupToken,
|
|
265
|
+
weT("删除轮播列表「{name}」?此操作不可恢复。", { name: (group && group.name) || "" }),
|
|
266
|
+
onDeleteGroup, onDisarmConfirm),
|
|
267
|
+
editing && React.createElement("div", { className: "we-picker__editor" },
|
|
268
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
269
|
+
React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("名称")),
|
|
270
|
+
React.createElement("input", {
|
|
271
|
+
className: "we-picker__text", type: "text",
|
|
272
|
+
value: editing.name,
|
|
273
|
+
"aria-label": weT("轮播列表名称"),
|
|
274
|
+
onInput: onEditName,
|
|
275
|
+
}),
|
|
276
|
+
),
|
|
277
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
278
|
+
React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("间隔")),
|
|
279
|
+
React.createElement("select", {
|
|
280
|
+
className: "we-picker__rotation-interval",
|
|
281
|
+
value: String(editing.interval),
|
|
282
|
+
onChange: onEditInterval,
|
|
283
|
+
// 推进方式选了「视频播完」时,间隔不参与推进 —— 置灰但保留可见(用户切回来时还在)
|
|
284
|
+
disabled: editing.trigger === "videoEnd",
|
|
285
|
+
"aria-label": weT("轮播间隔"),
|
|
286
|
+
},
|
|
287
|
+
...INTERVALS.map((minutes) =>
|
|
288
|
+
React.createElement("option", { key: minutes, value: String(minutes) }, weT("{min} 分钟", { min: minutes })),
|
|
289
|
+
)),
|
|
290
|
+
React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("推进")),
|
|
291
|
+
React.createElement("select", {
|
|
292
|
+
className: "we-picker__playlist-select",
|
|
293
|
+
value: editing.trigger === "videoEnd" ? "videoEnd" : "interval",
|
|
294
|
+
onChange: onEditTrigger,
|
|
295
|
+
"aria-label": weT("轮播推进方式"),
|
|
296
|
+
},
|
|
297
|
+
React.createElement("option", { value: "interval" }, weT("按间隔")),
|
|
298
|
+
React.createElement("option", { value: "videoEnd" }, weT("视频播完就换")),
|
|
299
|
+
),
|
|
300
|
+
React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("顺序")),
|
|
301
|
+
React.createElement("select", {
|
|
302
|
+
className: "we-picker__playlist-select",
|
|
303
|
+
value: editing.order,
|
|
304
|
+
onChange: onEditOrder,
|
|
305
|
+
"aria-label": weT("轮播顺序"),
|
|
306
|
+
},
|
|
307
|
+
React.createElement("option", { value: "sequence" }, weT("顺序", null, "seq")),
|
|
308
|
+
React.createElement("option", { value: "random" }, weT("随机")),
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
// 选片走**页内下钻**(与「选择壁纸」同一套库视图):点按钮进库浏览,
|
|
312
|
+
// 卡片点击 = 加入/移出草稿(pickerDraft),顶部提示已选数;「返回」回编辑器。
|
|
313
|
+
// 这里**只有一行入口按钮、没有内联网格**:大库在 24px 缩略图里翻页选片不可用,
|
|
314
|
+
// 全尺寸浏览 + 搜索/过滤才是选片的正确形态。
|
|
315
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
316
|
+
React.createElement("button", {
|
|
317
|
+
className: "we-picker__btn we-picker__btn--primary", type: "button",
|
|
318
|
+
onClick: onOpenPickerDraft,
|
|
319
|
+
}, weT("选择壁纸")),
|
|
320
|
+
React.createElement("span", { className: "we-picker__hint" },
|
|
321
|
+
weT("下钻进库挑选 · 点卡片加入 / 移出")),
|
|
322
|
+
),
|
|
323
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
324
|
+
React.createElement("span", { className: "we-picker__hint" }, weT("已选 {count} 个", { count: editing.wallpaperIds.length })),
|
|
325
|
+
sel.inventory.playlists.length > 0 && React.createElement("select", {
|
|
326
|
+
className: "we-picker__playlist-select",
|
|
327
|
+
value: "",
|
|
328
|
+
onChange: (e) => {
|
|
329
|
+
const p = sel.inventory.playlists.find((pl) => pl.id === e.target.value);
|
|
330
|
+
if (p) importPlaylistIntoDraft(p);
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
React.createElement("option", { value: "" }, weT("从 WE 播放列表导入…")),
|
|
334
|
+
...sel.inventory.playlists.map((p) => React.createElement("option", {
|
|
335
|
+
key: p.id, value: p.id,
|
|
336
|
+
}, weT("{name}({count} 可播放)", { name: p.name, count: p.portableCount || 0 }))),
|
|
337
|
+
),
|
|
338
|
+
),
|
|
339
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
340
|
+
React.createElement("button", {
|
|
341
|
+
className: "we-picker__btn", type: "button",
|
|
342
|
+
onClick: saveEditingGroup,
|
|
343
|
+
}, weT("保存")),
|
|
344
|
+
React.createElement("button", {
|
|
345
|
+
className: "we-picker__btn", type: "button",
|
|
346
|
+
onClick: cancelEditGroup,
|
|
347
|
+
}, weT("取消")),
|
|
348
|
+
),
|
|
349
|
+
),
|
|
350
|
+
// 图内过场:图片 / 幻灯片壁纸**内部**多张图之间的切换间隔。与上面「自动轮转」
|
|
351
|
+
//(壁纸与壁纸之间、分钟)是两件事 —— 这一项单位是秒,0 = 跟随 WE 项目里
|
|
352
|
+
// `imageswitchtimes` 声明的那一档。只有当前这张真是「图集 ≥2 张」的图片项目才可用,
|
|
353
|
+
// 其余档置灰并说明原因(常驻不隐藏:位置稳定,切到图片项目时不跳)。
|
|
354
|
+
React.createElement("div", { className: "we-picker__ctl" },
|
|
355
|
+
ctlText(weT("图片轮播"),
|
|
356
|
+
(sel.type === "image" && sel.slidesUrl)
|
|
357
|
+
? weT("幻灯片壁纸内部多张图之间的切换间隔;「跟随壁纸」= 用 WE 里设的周期")
|
|
358
|
+
: weT("当前壁纸不是多图图片项目,此项暂不生效")),
|
|
359
|
+
React.createElement("div", { className: "we-picker__ctl-side" },
|
|
360
|
+
React.createElement("select", {
|
|
361
|
+
className: "we-picker__picture-slide-interval",
|
|
362
|
+
value: String(sel.pictureSlideSeconds || 0),
|
|
363
|
+
onChange: onPictureSlideSeconds,
|
|
364
|
+
disabled: !(sel.type === "image" && sel.slidesUrl),
|
|
365
|
+
"aria-label": weT("图片轮播间隔"),
|
|
366
|
+
},
|
|
367
|
+
...pictureSlideOptions.map((sec) =>
|
|
368
|
+
React.createElement("option", { key: sec, value: String(sec) },
|
|
369
|
+
sec === 0 ? weT("跟随壁纸") : (sec < 60 ? weT("{n} 秒", { n: sec }) : weT("{n} 分钟", { n: sec / 60 }))),
|
|
370
|
+
)),
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
// #255②:「自动轮转」是**列表的**开关 —— 没选列表 / 列表编辑器展开时整行不渲染。
|
|
374
|
+
// 旧实现让这一行常驻,没列表时只能显示一句「请先选择或新建一个轮播列表」,
|
|
375
|
+
// 相当于在设置页里放了一行永远点不动的控件。
|
|
376
|
+
(sel.rotationGroupId && !editing) && React.createElement("div", { className: "we-picker__ctl" },
|
|
377
|
+
ctlText(weT("自动轮转"),
|
|
378
|
+
playableCount < 2
|
|
379
|
+
? weT("当前列表至少需要 2 个可播放壁纸")
|
|
380
|
+
: (group && group.trigger === "videoEnd")
|
|
381
|
+
? weT("当前视频播完就换下一张(不看间隔)")
|
|
382
|
+
: weT("每 {min} 分钟切换一次", { min: group ? group.interval : rotationIntervalDefault })),
|
|
383
|
+
React.createElement("div", { className: "we-picker__ctl-side" },
|
|
384
|
+
React.createElement("select", {
|
|
385
|
+
className: "we-picker__rotation-interval",
|
|
386
|
+
value: String(group ? group.interval : rotationIntervalDefault),
|
|
387
|
+
onChange: onGroupInterval,
|
|
388
|
+
disabled: !sel.rotationEnabled || !sel.rotationGroupId || playableCount < 2
|
|
389
|
+
|| (group && group.trigger === "videoEnd"),
|
|
390
|
+
"aria-label": weT("轮转间隔"),
|
|
391
|
+
},
|
|
392
|
+
...INTERVALS.map((minutes) =>
|
|
393
|
+
React.createElement("option", { key: minutes, value: String(minutes) }, weT("{min} 分钟", { min: minutes })),
|
|
394
|
+
)),
|
|
395
|
+
// 推进方式:按间隔 / 视频播完就换。常驻行直接可改(不必进「编辑」),
|
|
396
|
+
// 与列表编辑器里的同名字段(onEditTrigger)写同一个 group.trigger。
|
|
397
|
+
React.createElement("select", {
|
|
398
|
+
className: "we-picker__playlist-select",
|
|
399
|
+
value: group && group.trigger === "videoEnd" ? "videoEnd" : "interval",
|
|
400
|
+
onChange: onGroupTrigger,
|
|
401
|
+
disabled: !sel.rotationGroupId,
|
|
402
|
+
"aria-label": weT("轮播推进方式"),
|
|
403
|
+
},
|
|
404
|
+
React.createElement("option", { value: "interval" }, weT("按间隔")),
|
|
405
|
+
React.createElement("option", { value: "videoEnd" }, weT("视频播完就换")),
|
|
406
|
+
),
|
|
407
|
+
Toggle({
|
|
408
|
+
checked: sel.rotationEnabled,
|
|
409
|
+
onChange: onToggleRotation,
|
|
410
|
+
label: weT("自动轮转"),
|
|
411
|
+
disabled: !sel.rotationGroupId || playableCount < 2,
|
|
412
|
+
title: weT("按列表顺序/随机自动切换壁纸"),
|
|
413
|
+
}),
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── 内联模块:packages/media-bridge/src/wallpaper-uploads-section.js —— 「壁纸库」页签第四节「自定义壁纸」的渲染器(ADR-0013/ADR-0014 子包出户);入口的 registerPanelSection 把它的 render 登记进面板节注册表(library,order 40)(构建期注入;勿手改本段)──
|
|
421
|
+
/**
|
|
422
|
+
* wallpaper-uploads-section.js — 「壁纸库」页签第四节「自定义壁纸」的渲染器。
|
|
423
|
+
*
|
|
424
|
+
* 父页签 renderWallpaperTab 仍在主包组装点(`src/panel-tabs.js`),按面板节注册表
|
|
425
|
+
* (`src/extension-registry.js`)的顺序续接本包的节(library / order 40)。
|
|
426
|
+
*
|
|
427
|
+
* 归属(ADR-0013 D20):随包走的是**渲染器**;上传目录 / 官方资源目录 / 上传列表 /
|
|
428
|
+
* 确认令牌这些键、默认值、持久化与动作体全留主包,本文件只读 ctx 值 + 经 host 显式键集
|
|
429
|
+
* (入口 src/client.js 的 WALLPAPER_UPLOADS_HOST_REQUIRED)调主包动作。
|
|
430
|
+
*
|
|
431
|
+
* 唯一跨包通道 = 主 bundle 的显式键集(ADR-0014 S5.1 / S5.4);本文件里出现的 React / weT /
|
|
432
|
+
* changeUploadDir / changeWeAssetsDir / uploadWallpaperFile / removeUploadWallpaper /
|
|
433
|
+
* renderConfirmRow 都会由构建期配方内联后的入口从 host 解构进**同一个工厂作用域**。
|
|
434
|
+
*/
|
|
435
|
+
function renderWallpaperUploadsSection(ctx) {
|
|
436
|
+
const { armedConfirm, group, onArmConfirm, onCancelEditUploadDir, onCancelEditWeAssetsDir, onDisarmConfirm, onStartEditUploadDir, onStartEditWeAssetsDir, onUploadDirDraft, onWeAssetsDirDraft, playableCount, playableList, sel, uploadedList } = ctx;
|
|
437
|
+
const uploadToken = (w) => "upload:" + w.id;
|
|
438
|
+
return React.createElement(React.Fragment, null,
|
|
439
|
+
// ── 自定义壁纸: local JPG/PNG/MP4 as wallpapers. Files are written by the
|
|
440
|
+
// host into its plugin-managed directory and served through the same
|
|
441
|
+
// media/preview routes (read-A storage: survives restarts, no quota
|
|
442
|
+
// limits). Uploads merge into the inventory on the host side. ──
|
|
443
|
+
React.createElement("div", { className: "we-picker__section" },
|
|
444
|
+
React.createElement("div", { className: "we-picker__section-head" },
|
|
445
|
+
React.createElement("span", { className: "we-picker__section-label" }, weT("自定义壁纸")),
|
|
446
|
+
),
|
|
447
|
+
React.createElement("div", { className: "we-picker__uploads" },
|
|
448
|
+
// Storage location — users can point uploads at a non-system drive
|
|
449
|
+
// (most people don't want wallpaper files piling up on C:). The host
|
|
450
|
+
// persists the choice and migrates existing files on change.
|
|
451
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
452
|
+
React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("存储位置")),
|
|
453
|
+
React.createElement("span", {
|
|
454
|
+
className: "we-picker__uploads-path",
|
|
455
|
+
}, sel.inventory.uploadDir || "—"),
|
|
456
|
+
React.createElement("button", {
|
|
457
|
+
className: "we-picker__btn", type: "button",
|
|
458
|
+
disabled: sel.uploading,
|
|
459
|
+
onClick: onStartEditUploadDir,
|
|
460
|
+
}, weT("更改")),
|
|
461
|
+
),
|
|
462
|
+
sel.editingUploadDir && React.createElement("div", { className: "we-picker__row" },
|
|
463
|
+
React.createElement("input", {
|
|
464
|
+
className: "we-picker__text", type: "text",
|
|
465
|
+
value: sel.uploadDirDraft,
|
|
466
|
+
placeholder: weT("绝对路径,如 D:\\MyWallpapers"),
|
|
467
|
+
onInput: onUploadDirDraft,
|
|
468
|
+
onKeyDown: (e) => {
|
|
469
|
+
if (e.key === "Enter") changeUploadDir(sel.uploadDirDraft, true);
|
|
470
|
+
if (e.key === "Escape") onCancelEditUploadDir();
|
|
471
|
+
},
|
|
472
|
+
}),
|
|
473
|
+
React.createElement("button", {
|
|
474
|
+
className: "we-picker__btn", type: "button",
|
|
475
|
+
disabled: sel.uploading,
|
|
476
|
+
onClick: () => changeUploadDir(sel.uploadDirDraft, true),
|
|
477
|
+
}, weT("保存")),
|
|
478
|
+
React.createElement("button", {
|
|
479
|
+
className: "we-picker__btn", type: "button",
|
|
480
|
+
onClick: onCancelEditUploadDir,
|
|
481
|
+
}, weT("取消")),
|
|
482
|
+
),
|
|
483
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
484
|
+
React.createElement("span", { className: "we-picker__hint" },
|
|
485
|
+
weT("已有文件会迁移到新位置")),
|
|
486
|
+
React.createElement("span", { className: "we-picker__hint" },
|
|
487
|
+
weT("支持 ~ 表示用户主目录")),
|
|
488
|
+
),
|
|
489
|
+
// ── 官方资源路径(local-assets):场景实时渲染的 util、particle、
|
|
490
|
+
// gradient 贴图默认是程序化复刻(观感近似);指向本机 WE 安装目录的
|
|
491
|
+
// assets 树后按名取官方像素,与官方引擎逐像素对齐。素材属 WE 版权
|
|
492
|
+
// 内容,只从本机路径只读取用,不会被复制/上传。路径不硬编码 ——
|
|
493
|
+
// 持久化在宿主 config.json(weAssetsDir),经 we-assets-dir 端点读写。
|
|
494
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
495
|
+
React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("官方资源路径")),
|
|
496
|
+
React.createElement("span", {
|
|
497
|
+
className: "we-picker__uploads-path",
|
|
498
|
+
}, sel.inventory.weAssetsDir || "—"),
|
|
499
|
+
React.createElement("button", {
|
|
500
|
+
className: "we-picker__btn", type: "button",
|
|
501
|
+
onClick: onStartEditWeAssetsDir,
|
|
502
|
+
}, sel.inventory.weAssetsDir ? weT("更改") : weT("设置")),
|
|
503
|
+
),
|
|
504
|
+
sel.editingWeAssetsDir && React.createElement("div", { className: "we-picker__row" },
|
|
505
|
+
React.createElement("input", {
|
|
506
|
+
className: "we-picker__text", type: "text",
|
|
507
|
+
value: sel.weAssetsDirDraft,
|
|
508
|
+
placeholder: weT("WE assets 绝对路径,留空保存=清除"),
|
|
509
|
+
onInput: onWeAssetsDirDraft,
|
|
510
|
+
onKeyDown: (e) => {
|
|
511
|
+
if (e.key === "Enter") changeWeAssetsDir(sel.weAssetsDirDraft);
|
|
512
|
+
if (e.key === "Escape") onCancelEditWeAssetsDir();
|
|
513
|
+
},
|
|
514
|
+
}),
|
|
515
|
+
React.createElement("button", {
|
|
516
|
+
className: "we-picker__btn", type: "button",
|
|
517
|
+
onClick: () => changeWeAssetsDir(sel.weAssetsDirDraft),
|
|
518
|
+
}, weT("保存")),
|
|
519
|
+
React.createElement("button", {
|
|
520
|
+
className: "we-picker__btn", type: "button",
|
|
521
|
+
onClick: onCancelEditWeAssetsDir,
|
|
522
|
+
}, weT("取消")),
|
|
523
|
+
),
|
|
524
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
525
|
+
React.createElement("span", { className: "we-picker__hint" },
|
|
526
|
+
sel.inventory.weAssetsAvailable
|
|
527
|
+
? weT("已启用 · 场景实时渲染取官方像素")
|
|
528
|
+
: (sel.inventory.weAssetsDir
|
|
529
|
+
? weT("目录不可用(缺 materials/),场景实时渲染走程序化复刻")
|
|
530
|
+
: weT("未配置 · 场景实时渲染贴图走程序化复刻"))),
|
|
531
|
+
React.createElement("span", { className: "we-picker__hint" },
|
|
532
|
+
weT("指向 Wallpaper Engine 安装目录的 assets(或其拷贝)")),
|
|
533
|
+
),
|
|
534
|
+
sel.weAssetsError && React.createElement("div", { className: "we-picker__error" },
|
|
535
|
+
sel.weAssetsError,
|
|
536
|
+
),
|
|
537
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
538
|
+
React.createElement("span", { className: "we-picker__hint we-picker__label" }, weT("自定义")),
|
|
539
|
+
React.createElement("input", {
|
|
540
|
+
className: "we-picker__file", type: "file",
|
|
541
|
+
accept: ".jpg,.jpeg,.png,.mp4",
|
|
542
|
+
disabled: sel.uploading,
|
|
543
|
+
onChange: (e) => {
|
|
544
|
+
const f = e.target.files && e.target.files[0];
|
|
545
|
+
if (f) uploadWallpaperFile(f);
|
|
546
|
+
e.target.value = "";
|
|
547
|
+
},
|
|
548
|
+
}),
|
|
549
|
+
sel.uploading && React.createElement("span", { className: "we-picker__hint" }, weT("上传中…")),
|
|
550
|
+
),
|
|
551
|
+
sel.uploadError && React.createElement("div", { className: "we-picker__error" }, sel.uploadError),
|
|
552
|
+
sel.uploadNote && React.createElement("div", { className: "we-picker__note" }, sel.uploadNote),
|
|
553
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
554
|
+
React.createElement("span", { className: "we-picker__hint" }, weT("已上传 {count} 个", { count: uploadedList.length })),
|
|
555
|
+
React.createElement("span", { className: "we-picker__hint" }, weT("支持 JPG / PNG / MP4,及含 project.json 的 WE 壁纸目录")),
|
|
556
|
+
),
|
|
557
|
+
uploadedList.length > 0 && React.createElement("div", { className: "we-picker__uploads-list" },
|
|
558
|
+
uploadedList.map((w) => React.createElement(React.Fragment, { key: w.id },
|
|
559
|
+
React.createElement("div", { className: "we-picker__uploads-item" },
|
|
560
|
+
React.createElement("span", { className: "we-picker__uploads-name", title: w.title }, w.title),
|
|
561
|
+
React.createElement("span", { className: "we-picker__hint" }, w.type === "video" ? "MP4" : weT("图片")),
|
|
562
|
+
React.createElement("button", {
|
|
563
|
+
className: "we-picker__btn", type: "button",
|
|
564
|
+
// **会删本地文件** ⇒ 这一处最需要两下:第一下只置令牌。
|
|
565
|
+
onClick: () => onArmConfirm(uploadToken(w)),
|
|
566
|
+
disabled: sel.uploading || armedConfirm === uploadToken(w),
|
|
567
|
+
title: armedConfirm === uploadToken(w)
|
|
568
|
+
? weT("已经问过你了 —— 在下面那一行选「确认」或「取消」")
|
|
569
|
+
: weT("移除这个自定义壁纸(会再问一次,且会删除本地文件)"),
|
|
570
|
+
}, weT("移除")),
|
|
571
|
+
),
|
|
572
|
+
// 问句行紧跟它自己那一项(挂在同一个 `key` 下,位置不会串到别的壁纸那里)。
|
|
573
|
+
renderConfirmRow(armedConfirm, uploadToken(w),
|
|
574
|
+
weT("移除自定义壁纸「{name}」?此操作会删除本地文件,且不可恢复。", { name: w.title }),
|
|
575
|
+
() => removeUploadWallpaper(w.id), onDisarmConfirm),
|
|
576
|
+
)),
|
|
577
|
+
),
|
|
578
|
+
),
|
|
579
|
+
),
|
|
580
|
+
React.createElement("div", { className: "we-picker__row" },
|
|
581
|
+
React.createElement("span", { className: "we-picker__hint" },
|
|
582
|
+
(group
|
|
583
|
+
? weT("列表「{name}」:{count} 项 · {playable} 可播放 · 每 {interval} 分钟 · {order}{rotating}", {
|
|
584
|
+
name: group.name,
|
|
585
|
+
count: group.wallpaperIds.length,
|
|
586
|
+
playable: playableCount,
|
|
587
|
+
interval: group.interval,
|
|
588
|
+
order: group.order === "random" ? weT("随机") : weT("顺序", null, "seq"),
|
|
589
|
+
rotating: sel.rotationEnabled ? weT(" · 自动轮转中") : "",
|
|
590
|
+
})
|
|
591
|
+
: weT("{count} 个可播放壁纸{rotating}", { count: playableList.length, rotating: sel.rotationEnabled ? weT(" · 自动轮转中") : "" }))),
|
|
592
|
+
),
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 挂载台(test/verify-scene-live.mjs 等)按文件 import 本模块时没有构建期内联作用域,
|
|
597
|
+
// 所以必须显式导出;构建期配方(packages/media-bridge/client.build.json)会把这段 export 剥掉
|
|
598
|
+
// (与 packages/fonts/src/appearance-font-section.js 尾部同款)。
|
|
599
|
+
|
|
600
|
+
// ── 内联模块:packages/media-bridge/src/playback-rate-section.js —— 「播放」页签「倍速 / 帧率上限」节渲染器(含私有 renderTranscodeProgress;ADR-0013/ADR-0014 子包出户);入口的 registerPanelSection 把它登记进面板节注册表(playback,order 60)(构建期注入;勿手改本段)──
|
|
601
|
+
/**
|
|
602
|
+
* playback-rate-section.js — 「播放」页签的「倍速 / 帧率上限」节(含私有 renderTranscodeProgress)。
|
|
603
|
+
*
|
|
604
|
+
* ⚠️ 归属(ADR-0013 D20):随包走的只有**渲染器**;倍速 / 帧率上限的选择、默认值、持久化与
|
|
605
|
+
* 动作体(onPlaybackRate / onFpsCap)、档位表 FPS_CAP_VALUES、codecLabel、源信息与转码进度
|
|
606
|
+
* 状态全留在主包 ⇒ 值一律经主包构造的渲染 ctx 或 host 显式键集拿到。
|
|
607
|
+
* 本文件只读 ctx、不写 selection、不自发通知(接缝判据见 test/verify-client.mjs)。
|
|
608
|
+
* 构建期内联在 packages/media-bridge/src/client.js 之前(inline 顺序见 client.build.json)。
|
|
609
|
+
*/
|
|
610
|
+
|
|
611
|
+
// ── 转码进度行 ──
|
|
612
|
+
function renderTranscodeProgress(sel) {
|
|
613
|
+
return React.createElement("div", { className: "we-picker__row we-picker__prog", key: "transcode-prog" },
|
|
614
|
+
React.createElement("div", {
|
|
615
|
+
className: "we-picker__prog-track",
|
|
616
|
+
role: "progressbar",
|
|
617
|
+
"aria-label": weT("转码进度"),
|
|
618
|
+
"aria-valuemin": 0,
|
|
619
|
+
"aria-valuemax": 100,
|
|
620
|
+
"aria-valuenow": Math.max(0, Math.min(100, sel.transcodeProgress.percent || 0)),
|
|
621
|
+
},
|
|
622
|
+
React.createElement("div", {
|
|
623
|
+
className: "we-picker__prog-bar",
|
|
624
|
+
style: { width: Math.max(2, Math.min(100, sel.transcodeProgress.percent || 0)) + "%" },
|
|
625
|
+
}),
|
|
626
|
+
),
|
|
627
|
+
React.createElement("span", { className: "we-picker__hint" },
|
|
628
|
+
sel.transcodeProgress.phase === "download"
|
|
629
|
+
? weT("下载 ffmpeg {percent}%", { percent: sel.transcodeProgress.percent || 0 })
|
|
630
|
+
: sel.transcodeProgress.phase === "transcode" && sel.transcodeProgress.finalizing ? weT("收尾中…")
|
|
631
|
+
: sel.transcodeProgress.phase === "transcode"
|
|
632
|
+
? weT("转码中 {percent}%{eta}", {
|
|
633
|
+
percent: sel.transcodeProgress.percent || 0,
|
|
634
|
+
eta: sel.transcodeProgress.eta ? weT(" · 约剩 {sec} 秒", { sec: sel.transcodeProgress.eta }) : "",
|
|
635
|
+
})
|
|
636
|
+
: sel.transcodeProgress.phase === "done" ? weT("即将完成…")
|
|
637
|
+
: weT("准备中…"),
|
|
638
|
+
),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function renderEffectsPlaybackSection(ctx) {
|
|
643
|
+
const { onFpsCap, onPlaybackRate, sel, surface } = ctx;
|
|
644
|
+
const sidebarSurface = surface === "sidebar";
|
|
645
|
+
return React.createElement(React.Fragment, null,
|
|
646
|
+
// Playback speed — native playbackRate, instant, no media reload. Video
|
|
647
|
+
// wallpapers only (web/iframe and scene wallpapers have no playbackRate).
|
|
648
|
+
sel.type === "video"
|
|
649
|
+
&& React.createElement("div", { className: "we-picker__ctl", key: "rate" },
|
|
650
|
+
ctlText(weT("倍速")),
|
|
651
|
+
React.createElement("div", { className: "we-picker__seg" },
|
|
652
|
+
[0.5, 0.75, 1, 1.25, 1.5, 2].map((rate) =>
|
|
653
|
+
React.createElement("button", {
|
|
654
|
+
key: rate,
|
|
655
|
+
className: "we-picker__btn we-picker__rate" + (sel.playbackRate === rate ? " we-picker__rate--active" : ""),
|
|
656
|
+
type: "button",
|
|
657
|
+
onClick: () => onPlaybackRate(rate),
|
|
658
|
+
}, String(rate).replace(/\.?0+$/, "") + "x"),
|
|
659
|
+
),
|
|
660
|
+
),
|
|
661
|
+
),
|
|
662
|
+
// 解码帧率上限(抽帧转码):host 一次性把源视频重编码为上限帧率(时间线
|
|
663
|
+
// 1.0x 正常速度,解码占用随帧率线性下降),与倍速解耦。首次转码需等待,
|
|
664
|
+
// 播放中原片、转好自动切换;无 ffmpeg 自动回退原片。
|
|
665
|
+
// 侧栏档不出现在这里(准备与诊断,见函数头)。──
|
|
666
|
+
!sidebarSurface && sel.type === "video"
|
|
667
|
+
&& React.createElement("div", { className: "we-picker__ctl", key: "fps" },
|
|
668
|
+
ctlText(weT("帧率上限"), weT("抽帧转码 · 降低解码占用")),
|
|
669
|
+
React.createElement("div", { className: "we-picker__seg" },
|
|
670
|
+
FPS_CAP_VALUES.map((cap) =>
|
|
671
|
+
React.createElement("button", {
|
|
672
|
+
key: cap,
|
|
673
|
+
className: "we-picker__btn we-picker__rate" + (sel.fpsCap === cap ? " we-picker__rate--active" : ""),
|
|
674
|
+
type: "button",
|
|
675
|
+
onClick: () => onFpsCap(cap),
|
|
676
|
+
}, cap === 0 ? weT("无限制") : cap + "fps"),
|
|
677
|
+
),
|
|
678
|
+
),
|
|
679
|
+
),
|
|
680
|
+
// Source metadata + transcode status (host moov probe / transcode lifecycle).
|
|
681
|
+
// 源信息与转码进度同样是准备/诊断行 —— 侧栏档不画(见函数头)。
|
|
682
|
+
!sidebarSurface && sel.type === "video" && sel.mediaInfo && React.createElement("span", { className: "we-picker__hint", key: "media-info" },
|
|
683
|
+
weT("源 {width}×{height}{fps}{codec}{state}", {
|
|
684
|
+
width: sel.mediaInfo.width,
|
|
685
|
+
height: sel.mediaInfo.height,
|
|
686
|
+
fps: sel.mediaInfo.fps ? " · " + sel.mediaInfo.fps + "fps" : "",
|
|
687
|
+
codec: sel.mediaInfo.codec ? " · " + weT(codecLabel(sel.mediaInfo.codec)) : "",
|
|
688
|
+
state: sel.transcodeState === "working" ? weT(" · 抽帧准备中…")
|
|
689
|
+
: sel.transcodeState === "ready" ? weT(" · 已切换至 {fps}fps 抽帧版(正常速度,解码占用约减半)", { fps: sel.fpsCap })
|
|
690
|
+
: sel.transcodeState === "cached" ? weT(" · 抽帧版已就绪(下次切换到这张时生效)")
|
|
691
|
+
: sel.transcodeState === "native" ? weT(" · 源帧率未知,未抽帧")
|
|
692
|
+
: sel.transcodeState === "fallback" ? weT(" · 转码不可用,已回退原片")
|
|
693
|
+
: sel.transcodeState === "skipped" ? weT(" · 源帧率 ≤ 上限,无需抽帧")
|
|
694
|
+
: "",
|
|
695
|
+
}),
|
|
696
|
+
),
|
|
697
|
+
// Download / transcode progress bar (polled from /transcode-progress).
|
|
698
|
+
!sidebarSurface && sel.type === "video" && sel.transcodeState === "working" && sel.transcodeProgress
|
|
699
|
+
&& renderTranscodeProgress(sel),
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ── 内联模块:packages/media-bridge/src/wallpaper-fit-section.js —— 「播放」页签「适配 / 水平翻转」节渲染器(ADR-0013/ADR-0014 子包出户);入口的 registerPanelSection 把它登记进面板节注册表(playback,order 70)(构建期注入;勿手改本段)──
|
|
704
|
+
/**
|
|
705
|
+
* wallpaper-fit-section.js — 「播放」页签的「适配 / 水平翻转」节渲染器。
|
|
706
|
+
*
|
|
707
|
+
* ⚠️ 归属(ADR-0013 D20):随包走的只有**渲染器**;objectFit / flip 的选择、默认值、持久化与
|
|
708
|
+
* 动作体(onObjectFit / onFlip)全留在主包 ⇒ 值一律经主包构造的渲染 ctx 拿到。
|
|
709
|
+
* 本文件只读 ctx、不写 selection、不自发通知(接缝判据见 test/verify-client.mjs)。
|
|
710
|
+
* 构建期内联在 packages/media-bridge/src/client.js 之前(inline 顺序见 client.build.json)。
|
|
711
|
+
*/
|
|
712
|
+
|
|
713
|
+
function renderEffectsFitSection(ctx) {
|
|
714
|
+
const { onFlip, onObjectFit, sel } = ctx;
|
|
715
|
+
return React.createElement(React.Fragment, null,
|
|
716
|
+
// Fit mode — applies to the CURRENT wallpaper whatever its type (WE
|
|
717
|
+
// video/scene image and custom uploads alike; web/iframe wallpapers
|
|
718
|
+
// have no object-fit). 覆盖=cover 填充=contain 居中=center 拉伸=fill
|
|
719
|
+
React.createElement("div", { className: "we-picker__ctl", key: "fit" },
|
|
720
|
+
ctlText(weT("适配")),
|
|
721
|
+
React.createElement("div", { className: "we-picker__seg" },
|
|
722
|
+
["cover", "contain", "center", "fill"].map((mode) => {
|
|
723
|
+
const label = { cover: weT("覆盖"), contain: weT("填充"), center: weT("居中"), fill: weT("拉伸") }[mode];
|
|
724
|
+
return React.createElement("button", {
|
|
725
|
+
key: mode,
|
|
726
|
+
className: "we-picker__btn we-picker__rate" + (sel.objectFit === mode ? " we-picker__rate--active" : ""),
|
|
727
|
+
type: "button",
|
|
728
|
+
title: mode,
|
|
729
|
+
onClick: () => onObjectFit(mode),
|
|
730
|
+
}, label);
|
|
731
|
+
}),
|
|
732
|
+
),
|
|
733
|
+
),
|
|
734
|
+
// Horizontal mirror — scaleX(-1), compositor-only; works for video,
|
|
735
|
+
// web (iframe) and (later) uploaded image wallpapers alike.
|
|
736
|
+
switchRow(weT("水平翻转"), sel.flip, onFlip, { key: "flip" }),
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ── 客户端正文(packages/media-bridge/src/client.js)───────────────────────────
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* 缺键**大声失败**(ADR-0014 S5.1 / S5.4 显式键集 + ADR-0013 D3③「宁可显示、不可静默消失」)。
|
|
744
|
+
* 主包被关 ⇒ 本 bundle 根本不会被 require(ADR-0014 S7.2 连坐);能走到这里却缺键 = 键集与申报表脱钩。
|
|
745
|
+
*/
|
|
746
|
+
const WALLPAPER_SECTIONS_HOST_REQUIRED = [
|
|
747
|
+
// 渲染内核(主 bundle 的共享零件表)
|
|
748
|
+
'React', 'weT', 'ctlText',
|
|
749
|
+
// 播放配置两节(S3d):帧率上限档位表 / 编码显示名 / 开关行(倍速与适配的档位是节内字面量)
|
|
750
|
+
'FPS_CAP_VALUES', 'codecLabel', 'switchRow',
|
|
751
|
+
// 当前壁纸(第一节):转盘组件 / 转盘自旋开关 / 实时渲染失败原因(getter 直给函数引用)
|
|
752
|
+
'VinylRecord', 'vinylSpinVisible', 'liveFailReasonOf',
|
|
753
|
+
// 切换过场(第二节):类型表 / 方向表 / 速度档 / 当前生效值的活绑定
|
|
754
|
+
'switchTransitionOf', 'SWITCH_TRANSITIONS', 'SWITCH_DIRS', 'SWITCH_DIR_LABELS', 'SWITCH_SPEEDS',
|
|
755
|
+
// 自动轮播(第三节):列表 / 编辑草稿 / 选片下钻 / 开关 / 确认行 / 默认间隔窄键
|
|
756
|
+
'groupWallpapers', 'startCreateGroup', 'startEditGroup', 'saveEditingGroup', 'cancelEditGroup',
|
|
757
|
+
'importPlaylistIntoDraft', 'Toggle', 'rotationIntervalDefault',
|
|
758
|
+
// 自定义壁纸(第四节):上传目录 / 官方资源目录 / 上传 / 移除 / 确认行(动作体全留主包)
|
|
759
|
+
'changeUploadDir', 'changeWeAssetsDir', 'uploadWallpaperFile', 'removeUploadWallpaper', 'renderConfirmRow',
|
|
760
|
+
// 贡献面(主 bundle 的收件箱:值 + 回调,写权留主包)
|
|
761
|
+
'registerPanelSection',
|
|
762
|
+
];
|
|
763
|
+
for (const key of WALLPAPER_SECTIONS_HOST_REQUIRED) {
|
|
764
|
+
if (!(key in host)) {
|
|
765
|
+
throw new Error('[wallpaper-engine-media-bridge] 主 bundle 键集缺少 "' + key
|
|
766
|
+
+ '"(ADR-0014 S5.1 / S5.4 显式键集:要么补键,要么改申报表,不许静默 undefined)');
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const {
|
|
770
|
+
React, weT, ctlText, FPS_CAP_VALUES, codecLabel, switchRow,
|
|
771
|
+
VinylRecord, vinylSpinVisible, liveFailReasonOf,
|
|
772
|
+
switchTransitionOf, SWITCH_TRANSITIONS, SWITCH_DIRS, SWITCH_DIR_LABELS, SWITCH_SPEEDS,
|
|
773
|
+
groupWallpapers, startCreateGroup, startEditGroup, saveEditingGroup, cancelEditGroup,
|
|
774
|
+
importPlaylistIntoDraft, Toggle, rotationIntervalDefault,
|
|
775
|
+
changeUploadDir, changeWeAssetsDir, uploadWallpaperFile, removeUploadWallpaper, renderConfirmRow,
|
|
776
|
+
registerPanelSection,
|
|
777
|
+
} = host;
|
|
778
|
+
|
|
779
|
+
/* ===== 跨包登记:「壁纸库」页签第一/二/三/四节(全部走面板节注册表)===== */
|
|
780
|
+
|
|
781
|
+
// 主包 src/panel-tabs.js 的 `renderWallpaperTab(ctx)` 本身不再内置任何一节,只按注册表续接本包
|
|
782
|
+
// 登记的节(`wePanelSectionsOf('library')`,见 src/extension-registry.js)。三个硬口径:
|
|
783
|
+
// ① `title` 必须是 **getter**:注册发生在 bundle 求值期,那时 i18n 表 / `weT` 都还没到位,
|
|
784
|
+
// 注册期读词会把 `weT` 拉到求值期(字体包 / fx / parallax 的 title getter 同款理由)。
|
|
785
|
+
// ② order:current=10 / transition=20 / rotation=30 / uploads=40 —— 与搬前组装点里的调用序逐位
|
|
786
|
+
// 相同(S3c 起 current 也走注册表;主包组装点本身只画注册节,不再内置任何一节)。
|
|
787
|
+
// 顺序由 order 决定、不靠求值顺序。
|
|
788
|
+
// ③ 注册表只登记、不持有状态:ctx 由主包在渲染处构造(一个键都不删),本包不缓存、不自订阅。
|
|
789
|
+
registerPanelSection('library', 'wallpaper-current', {
|
|
790
|
+
id: 'wallpaper-current',
|
|
791
|
+
get title() { return weT("当前壁纸"); },
|
|
792
|
+
order: 10,
|
|
793
|
+
render: renderWallpaperCurrentSection,
|
|
794
|
+
});
|
|
795
|
+
registerPanelSection('library', 'wallpaper-transition', {
|
|
796
|
+
id: 'wallpaper-transition',
|
|
797
|
+
get title() { return weT("切换过场"); },
|
|
798
|
+
order: 20,
|
|
799
|
+
render: renderWallpaperTransitionSection,
|
|
800
|
+
});
|
|
801
|
+
registerPanelSection('library', 'wallpaper-rotation', {
|
|
802
|
+
id: 'wallpaper-rotation',
|
|
803
|
+
get title() { return weT("自动轮播"); },
|
|
804
|
+
order: 30,
|
|
805
|
+
render: renderWallpaperRotationSection,
|
|
806
|
+
});
|
|
807
|
+
registerPanelSection('library', 'wallpaper-uploads', {
|
|
808
|
+
id: 'wallpaper-uploads',
|
|
809
|
+
get title() { return weT("自定义壁纸"); },
|
|
810
|
+
order: 40,
|
|
811
|
+
render: renderWallpaperUploadsSection,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
/* ===== 跨包登记:「播放」页签的倍速/帧率上限、适配/翻转两节(P-D S3d)===== */
|
|
815
|
+
|
|
816
|
+
// 主包 src/panel-tabs.js 的 `renderEffectsTab(ctx)` 在三个内置节之后 append
|
|
817
|
+
// `wePanelSectionsOf('playback')` ⇒ 顺序仍由 order 决定(60 / 70),与搬前调用序逐位相同;
|
|
818
|
+
// 两节仍画在同一个「画面」节容器里(组装点把注册节放在内置节之后),视觉分组不变。
|
|
819
|
+
registerPanelSection('playback', 'wallpaper-playback-rate', {
|
|
820
|
+
id: 'wallpaper-playback-rate',
|
|
821
|
+
get title() { return weT("倍速"); },
|
|
822
|
+
order: 60,
|
|
823
|
+
render: renderEffectsPlaybackSection,
|
|
824
|
+
});
|
|
825
|
+
registerPanelSection('playback', 'wallpaper-fit', {
|
|
826
|
+
id: 'wallpaper-fit',
|
|
827
|
+
get title() { return weT("适配"); },
|
|
828
|
+
order: 70,
|
|
829
|
+
render: renderEffectsFitSection,
|
|
830
|
+
});
|
|
831
|
+
if (typeof exports.apply !== "function") exports.apply = () => undefined;
|
|
832
|
+
if (!Array.isArray(exports.inject)) exports.inject = [];
|
|
833
|
+
return module.exports;
|
|
834
|
+
},
|
|
835
|
+
});
|